From 64c9f9e1cb60690a890ab5f5a88e53746439a6a9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 6 Aug 2021 00:15:04 +0000 Subject: [PATCH 001/355] [ci skip] Translation update --- .../accuweather/translations/hu.json | 5 ++ .../components/acmeda/translations/hu.json | 8 ++ .../components/adax/translations/hu.json | 20 +++++ .../components/adguard/translations/hu.json | 6 +- .../components/agent_dvr/translations/hu.json | 3 +- .../components/airvisual/translations/hu.json | 18 ++++- .../airvisual/translations/sensor.hu.json | 20 +++++ .../alarm_control_panel/translations/hu.json | 7 +- .../alarmdecoder/translations/hu.json | 2 + .../ambiclimate/translations/hu.json | 11 +++ .../components/arcam_fmj/translations/hu.json | 10 ++- .../components/atag/translations/hu.json | 6 +- .../components/august/translations/hu.json | 1 + .../components/awair/translations/hu.json | 3 +- .../components/axis/translations/hu.json | 10 ++- .../azure_devops/translations/hu.json | 15 +++- .../components/blebox/translations/hu.json | 5 +- .../components/blink/translations/hu.json | 14 +++- .../components/braviatv/translations/hu.json | 2 + .../components/bsblan/translations/hu.json | 5 +- .../cert_expiry/translations/hu.json | 11 ++- .../components/co2signal/translations/hu.json | 34 ++++++++ .../components/control4/translations/hu.json | 10 +++ .../coolmaster/translations/hu.json | 11 ++- .../components/deconz/translations/hu.json | 11 ++- .../components/demo/translations/hu.json | 7 ++ .../components/denonavr/translations/hu.json | 28 ++++++- .../components/dexcom/translations/hu.json | 4 +- .../components/directv/translations/hu.json | 8 ++ .../components/doorbird/translations/hu.json | 17 +++- .../components/dunehd/translations/hu.json | 1 + .../components/eafm/translations/hu.json | 7 +- .../components/elgato/translations/hu.json | 7 +- .../components/elkm1/translations/hu.json | 11 ++- .../components/energy/translations/hu.json | 3 + .../components/enocean/translations/hu.json | 16 ++++ .../components/firmata/translations/hu.json | 4 + .../flick_electric/translations/hu.json | 2 + .../components/flipr/translations/hu.json | 30 +++++++ .../components/flume/translations/hu.json | 6 +- .../flunearyou/translations/hu.json | 3 +- .../forked_daapd/translations/hu.json | 13 ++- .../components/freebox/translations/hu.json | 4 + .../components/fritzbox/translations/hu.json | 4 +- .../geonetnz_quakes/translations/hu.json | 1 + .../components/gogogate2/translations/hu.json | 1 + .../growatt_server/translations/hu.json | 1 + .../components/guardian/translations/hu.json | 6 +- .../components/hangouts/translations/hu.json | 1 + .../components/harmony/translations/hu.json | 22 ++++- .../components/heos/translations/hu.json | 4 +- .../homeassistant/translations/he.json | 1 + .../homeassistant/translations/hu.json | 1 + .../components/homekit/translations/hu.json | 12 +++ .../homekit_controller/translations/hu.json | 2 + .../components/honeywell/translations/hu.json | 17 ++++ .../huawei_lte/translations/hu.json | 3 + .../components/hue/translations/hu.json | 22 ++++- .../translations/hu.json | 7 +- .../hvv_departures/translations/hu.json | 23 +++++- .../components/iaqualink/translations/hu.json | 4 +- .../components/icloud/translations/hu.json | 5 +- .../components/insteon/translations/fa.json | 7 ++ .../components/insteon/translations/hu.json | 27 ++++++- .../components/ipp/translations/hu.json | 14 +++- .../components/iqvia/translations/hu.json | 12 +++ .../islamic_prayer_times/translations/hu.json | 18 ++++- .../components/isy994/translations/hu.json | 13 ++- .../components/juicenet/translations/hu.json | 1 + .../components/konnected/translations/hu.json | 81 +++++++++++++++++-- .../components/kraken/translations/nl.json | 4 - .../components/life360/translations/hu.json | 4 +- .../components/litejet/translations/hu.json | 10 +++ .../logi_circle/translations/hu.json | 7 ++ .../lutron_caseta/translations/hu.json | 4 + .../components/melcloud/translations/hu.json | 7 +- .../minecraft_server/translations/hu.json | 5 +- .../components/monoprice/translations/hu.json | 26 +++++- .../components/mqtt/translations/hu.json | 20 ++++- .../components/myq/translations/hu.json | 3 +- .../components/netatmo/translations/hu.json | 16 +++- .../nfandroidtv/translations/hu.json | 21 +++++ .../components/nuheat/translations/hu.json | 4 +- .../components/nut/translations/hu.json | 25 +++++- .../components/nws/translations/hu.json | 7 +- .../components/onvif/translations/hu.json | 14 +++- .../opentherm_gw/translations/hu.json | 3 +- .../ovo_energy/translations/hu.json | 1 + .../components/ozw/translations/hu.json | 1 + .../panasonic_viera/translations/hu.json | 3 +- .../components/plex/translations/hu.json | 13 ++- .../components/powerwall/translations/hu.json | 6 +- .../components/prosegur/translations/hu.json | 29 +++++++ .../pvpc_hourly_pricing/translations/hu.json | 7 +- .../components/rachio/translations/hu.json | 11 +++ .../components/renault/translations/he.json | 3 +- .../components/renault/translations/hu.json | 27 +++++++ .../components/roku/translations/hu.json | 7 +- .../components/roomba/translations/hu.json | 2 + .../components/roon/translations/hu.json | 4 +- .../components/sense/translations/hu.json | 3 +- .../components/sentry/translations/hu.json | 3 +- .../components/shelly/translations/hu.json | 3 + .../simplisafe/translations/hu.json | 15 ++++ .../components/smappee/translations/hu.json | 12 ++- .../components/smarthab/translations/hu.json | 4 +- .../smartthings/translations/hu.json | 14 +++- .../components/solaredge/translations/hu.json | 3 +- .../components/solarlog/translations/hu.json | 6 +- .../components/soma/translations/hu.json | 1 + .../somfy_mylink/translations/hu.json | 3 + .../speedtestdotnet/translations/hu.json | 2 + .../squeezebox/translations/hu.json | 3 +- .../srp_energy/translations/hu.json | 1 + .../switcher_kis/translations/hu.json | 13 +++ .../components/syncthru/translations/hu.json | 3 +- .../synology_dsm/translations/hu.json | 18 ++++- .../components/tado/translations/hu.json | 15 +++- .../components/tesla/translations/hu.json | 2 + .../components/toon/translations/hu.json | 10 +++ .../totalconnect/translations/hu.json | 3 +- .../components/traccar/translations/hu.json | 6 ++ .../components/tractive/translations/ca.json | 19 +++++ .../components/tractive/translations/en.json | 2 +- .../components/tractive/translations/et.json | 19 +++++ .../components/tractive/translations/hu.json | 19 +++++ .../components/tractive/translations/it.json | 19 +++++ .../components/tractive/translations/pl.json | 19 +++++ .../components/tractive/translations/ru.json | 19 +++++ .../tractive/translations/zh-Hant.json | 19 +++++ .../transmission/translations/hu.json | 3 +- .../twentemilieu/translations/hu.json | 8 +- .../components/unifi/translations/hu.json | 37 ++++++++- .../components/upb/translations/hu.json | 7 +- .../components/upnp/translations/hu.json | 9 +++ .../uptimerobot/translations/ca.json | 20 +++++ .../uptimerobot/translations/de.json | 18 +++++ .../uptimerobot/translations/en.json | 2 +- .../uptimerobot/translations/et.json | 20 +++++ .../uptimerobot/translations/he.json | 18 +++++ .../uptimerobot/translations/hu.json | 20 +++++ .../uptimerobot/translations/it.json | 18 +++++ .../uptimerobot/translations/pl.json | 20 +++++ .../uptimerobot/translations/ru.json | 20 +++++ .../uptimerobot/translations/zh-Hant.json | 20 +++++ .../components/velbus/translations/hu.json | 9 +++ .../components/vera/translations/hu.json | 30 +++++++ .../components/vilfo/translations/hu.json | 1 + .../components/vizio/translations/hu.json | 19 ++++- .../components/wemo/translations/hu.json | 5 ++ .../components/wiffi/translations/hu.json | 4 +- .../components/withings/translations/hu.json | 1 + .../components/wolflink/translations/hu.json | 6 +- .../wolflink/translations/sensor.hu.json | 76 ++++++++++++++++- .../xiaomi_miio/translations/select.ca.json | 9 +++ .../xiaomi_miio/translations/select.he.json | 2 + .../xiaomi_miio/translations/select.hu.json | 9 +++ .../xiaomi_miio/translations/select.it.json | 9 +++ .../yale_smart_alarm/translations/hu.json | 28 +++++++ .../components/youless/translations/hu.json | 15 ++++ .../components/zha/translations/hu.json | 68 +++++++++++++++- 161 files changed, 1703 insertions(+), 129 deletions(-) create mode 100644 homeassistant/components/adax/translations/hu.json create mode 100644 homeassistant/components/airvisual/translations/sensor.hu.json create mode 100644 homeassistant/components/co2signal/translations/hu.json create mode 100644 homeassistant/components/energy/translations/hu.json create mode 100644 homeassistant/components/flipr/translations/hu.json create mode 100644 homeassistant/components/honeywell/translations/hu.json create mode 100644 homeassistant/components/insteon/translations/fa.json create mode 100644 homeassistant/components/nfandroidtv/translations/hu.json create mode 100644 homeassistant/components/prosegur/translations/hu.json create mode 100644 homeassistant/components/renault/translations/hu.json create mode 100644 homeassistant/components/switcher_kis/translations/hu.json create mode 100644 homeassistant/components/tractive/translations/ca.json create mode 100644 homeassistant/components/tractive/translations/et.json create mode 100644 homeassistant/components/tractive/translations/hu.json create mode 100644 homeassistant/components/tractive/translations/it.json create mode 100644 homeassistant/components/tractive/translations/pl.json create mode 100644 homeassistant/components/tractive/translations/ru.json create mode 100644 homeassistant/components/tractive/translations/zh-Hant.json create mode 100644 homeassistant/components/uptimerobot/translations/ca.json create mode 100644 homeassistant/components/uptimerobot/translations/de.json create mode 100644 homeassistant/components/uptimerobot/translations/et.json create mode 100644 homeassistant/components/uptimerobot/translations/he.json create mode 100644 homeassistant/components/uptimerobot/translations/hu.json create mode 100644 homeassistant/components/uptimerobot/translations/it.json create mode 100644 homeassistant/components/uptimerobot/translations/pl.json create mode 100644 homeassistant/components/uptimerobot/translations/ru.json create mode 100644 homeassistant/components/uptimerobot/translations/zh-Hant.json create mode 100644 homeassistant/components/vera/translations/hu.json create mode 100644 homeassistant/components/xiaomi_miio/translations/select.ca.json create mode 100644 homeassistant/components/xiaomi_miio/translations/select.hu.json create mode 100644 homeassistant/components/xiaomi_miio/translations/select.it.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/hu.json create mode 100644 homeassistant/components/youless/translations/hu.json diff --git a/homeassistant/components/accuweather/translations/hu.json b/homeassistant/components/accuweather/translations/hu.json index ce4721693f3..3cb78005d46 100644 --- a/homeassistant/components/accuweather/translations/hu.json +++ b/homeassistant/components/accuweather/translations/hu.json @@ -15,6 +15,7 @@ "longitude": "Hossz\u00fas\u00e1g", "name": "N\u00e9v" }, + "description": "Ha seg\u00edts\u00e9gre van sz\u00fcks\u00e9ge a konfigur\u00e1l\u00e1shoz, n\u00e9zze meg itt: https://www.home-assistant.io/integrations/accuweather/ \n\nEgyes \u00e9rz\u00e9kel\u0151k alap\u00e9rtelmez\u00e9s szerint nincsenek enged\u00e9lyezve. Az integr\u00e1ci\u00f3s konfigur\u00e1ci\u00f3 ut\u00e1n enged\u00e9lyezheti \u0151ket az entit\u00e1s-nyilv\u00e1ntart\u00e1sban.\nAz id\u0151j\u00e1r\u00e1s-el\u0151rejelz\u00e9s alap\u00e9rtelmez\u00e9s szerint nincs enged\u00e9lyezve. Ezt az integr\u00e1ci\u00f3s be\u00e1ll\u00edt\u00e1sokban enged\u00e9lyezheti.", "title": "AccuWeather" } } @@ -22,6 +23,10 @@ "options": { "step": { "user": { + "data": { + "forecast": "Id\u0151j\u00e1r\u00e1s el\u0151rejelz\u00e9s" + }, + "description": "Az AccuWeather API kulcs ingyenes verzi\u00f3j\u00e1nak korl\u00e1tai miatt, amikor enged\u00e9lyezi az id\u0151j\u00e1r\u00e1s -el\u0151rejelz\u00e9st, az adatfriss\u00edt\u00e9seket 40 percenk\u00e9nt 80 percenk\u00e9nt hajtj\u00e1k v\u00e9gre.", "title": "AccuWeather be\u00e1ll\u00edt\u00e1sok" } } diff --git a/homeassistant/components/acmeda/translations/hu.json b/homeassistant/components/acmeda/translations/hu.json index 6105977de80..f302995e7e9 100644 --- a/homeassistant/components/acmeda/translations/hu.json +++ b/homeassistant/components/acmeda/translations/hu.json @@ -2,6 +2,14 @@ "config": { "abort": { "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "step": { + "user": { + "data": { + "id": "Gazdag\u00e9p azonos\u00edt\u00f3" + }, + "title": "V\u00e1lassza ki a hozz\u00e1adni k\u00edv\u00e1nt hubot" + } } } } \ No newline at end of file diff --git a/homeassistant/components/adax/translations/hu.json b/homeassistant/components/adax/translations/hu.json new file mode 100644 index 00000000000..726381a4dd7 --- /dev/null +++ b/homeassistant/components/adax/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "user": { + "data": { + "account_id": "Fi\u00f3k ID", + "host": "Gazdag\u00e9p", + "password": "Jelsz\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/hu.json b/homeassistant/components/adguard/translations/hu.json index 22fb5539bfa..8a860caf79d 100644 --- a/homeassistant/components/adguard/translations/hu.json +++ b/homeassistant/components/adguard/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "existing_instance_updated": "Friss\u00edtette a megl\u00e9v\u0151 konfigur\u00e1ci\u00f3t." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" @@ -19,7 +20,8 @@ "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", "username": "Felhaszn\u00e1l\u00f3n\u00e9v", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" - } + }, + "description": "\u00c1ll\u00edtsa be az AdGuard Home p\u00e9ld\u00e1nyt, hogy lehet\u0151v\u00e9 tegye a fel\u00fcgyeletet \u00e9s az ir\u00e1ny\u00edt\u00e1st." } } } diff --git a/homeassistant/components/agent_dvr/translations/hu.json b/homeassistant/components/agent_dvr/translations/hu.json index 49968ceea75..fff86517073 100644 --- a/homeassistant/components/agent_dvr/translations/hu.json +++ b/homeassistant/components/agent_dvr/translations/hu.json @@ -12,7 +12,8 @@ "data": { "host": "Hoszt", "port": "Port" - } + }, + "title": "\u00c1ll\u00edtsa be az Agent DVR-t" } } } diff --git a/homeassistant/components/airvisual/translations/hu.json b/homeassistant/components/airvisual/translations/hu.json index e7c47e93793..043a2402283 100644 --- a/homeassistant/components/airvisual/translations/hu.json +++ b/homeassistant/components/airvisual/translations/hu.json @@ -34,13 +34,29 @@ "data": { "ip_address": "Hoszt", "password": "Jelsz\u00f3" - } + }, + "description": "Szem\u00e9lyes AirVisual egys\u00e9g figyel\u00e9se. A jelsz\u00f3 lek\u00e9rhet\u0151 a k\u00e9sz\u00fcl\u00e9k felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9r\u0151l.", + "title": "AirVisual Node/Pro konfigur\u00e1l\u00e1sa" }, "reauth_confirm": { "data": { "api_key": "API kulcs" }, "title": "Az AirVisual \u00fajb\u00f3li hiteles\u00edt\u00e9se" + }, + "user": { + "description": "V\u00e1lassza ki, hogy milyen t\u00edpus\u00fa AirVisual adatokat szeretne figyelni.", + "title": "Az AirVisual konfigur\u00e1l\u00e1sa" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "A megfigyelt f\u00f6ldrajz megjelen\u00edt\u00e9se a t\u00e9rk\u00e9pen" + }, + "title": "Az AirVisual konfigur\u00e1l\u00e1sa" } } } diff --git a/homeassistant/components/airvisual/translations/sensor.hu.json b/homeassistant/components/airvisual/translations/sensor.hu.json new file mode 100644 index 00000000000..93fbb2ce510 --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.hu.json @@ -0,0 +1,20 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "Sz\u00e9n-monoxid", + "n2": "Nitrog\u00e9n-dioxid", + "o3": "\u00d3zon", + "p1": "PM10", + "p2": "PM2.5", + "s2": "K\u00e9n-dioxid" + }, + "airvisual__pollutant_level": { + "good": "J\u00f3", + "hazardous": "Vesz\u00e9lyes", + "moderate": "M\u00e9rs\u00e9kelt", + "unhealthy": "Eg\u00e9szs\u00e9gtelen", + "unhealthy_sensitive": "Eg\u00e9szs\u00e9gtelen az \u00e9rz\u00e9keny csoportok sz\u00e1m\u00e1ra", + "very_unhealthy": "Nagyon eg\u00e9szs\u00e9gtelen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/translations/hu.json b/homeassistant/components/alarm_control_panel/translations/hu.json index 961006938d9..5eba25a9ec2 100644 --- a/homeassistant/components/alarm_control_panel/translations/hu.json +++ b/homeassistant/components/alarm_control_panel/translations/hu.json @@ -9,7 +9,12 @@ "trigger": "{entity_name} riaszt\u00e1si esem\u00e9ny ind\u00edt\u00e1sa" }, "condition_type": { - "is_armed_vacation": "{entity_name} nyaral\u00e1s \u00e9les\u00edtve" + "is_armed_away": "{entity_name} \u00e9les\u00edtve van", + "is_armed_home": "{entity_name} \u00e9les\u00edtett otthoni m\u00f3dban", + "is_armed_night": "{entity_name} \u00e9les\u00edtett \u00e9jszaka m\u00f3dban", + "is_armed_vacation": "{entity_name} nyaral\u00e1s \u00e9les\u00edtve", + "is_disarmed": "{entity_name} hat\u00e1stalan\u00edtva", + "is_triggered": "{entity_name} aktiv\u00e1lva van" }, "trigger_type": { "armed_away": "{entity_name} t\u00e1voz\u00f3 m\u00f3dban lett \u00e9les\u00edtve", diff --git a/homeassistant/components/alarmdecoder/translations/hu.json b/homeassistant/components/alarmdecoder/translations/hu.json index 47db325f06c..ace9c7059ca 100644 --- a/homeassistant/components/alarmdecoder/translations/hu.json +++ b/homeassistant/components/alarmdecoder/translations/hu.json @@ -31,6 +31,7 @@ "error": { "int": "Az al\u00e1bbi mez\u0151nek eg\u00e9sz sz\u00e1mnak kell lennie.", "loop_range": "Az RF hurok eg\u00e9sz sz\u00e1m\u00e1nak 1 \u00e9s 4 k\u00f6z\u00f6tt kell lennie.", + "loop_rfid": "Az RF hurok nem haszn\u00e1lhat\u00f3 RF sorozat n\u00e9lk\u00fcl.", "relay_inclusive": "A rel\u00e9c\u00edm \u00e9s a rel\u00e9csatorna egym\u00e1st\u00f3l f\u00fcgg, \u00e9s egy\u00fctt kell felt\u00fcntetni." }, "step": { @@ -55,6 +56,7 @@ "zone_name": "Z\u00f3na neve", "zone_relayaddr": "Rel\u00e9 c\u00edm", "zone_relaychan": "Rel\u00e9 csatorna", + "zone_rfid": "RF soros", "zone_type": "Z\u00f3na t\u00edpusa" }, "description": "Adja meg a {zone_number} z\u00f3na adatait. {zone_number} z\u00f3na t\u00f6rl\u00e9s\u00e9hez hagyja \u00fcresen a Z\u00f3na neve elemet.", diff --git a/homeassistant/components/ambiclimate/translations/hu.json b/homeassistant/components/ambiclimate/translations/hu.json index 04035f04cca..3898535c427 100644 --- a/homeassistant/components/ambiclimate/translations/hu.json +++ b/homeassistant/components/ambiclimate/translations/hu.json @@ -1,11 +1,22 @@ { "config": { "abort": { + "access_token": "Ismeretlen hiba a hozz\u00e1f\u00e9r\u00e9si token gener\u00e1l\u00e1s\u00e1ban.", "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t." }, "create_entry": { "default": "Sikeres hiteles\u00edt\u00e9s" + }, + "error": { + "follow_link": "K\u00e9rlek, k\u00f6vesd a hivatkoz\u00e1st \u00e9s hiteles\u00edtsd magad miel\u0151tt megnyomod a K\u00fcld\u00e9s gombot", + "no_token": "Nem hiteles\u00edtett Ambiclimate" + }, + "step": { + "auth": { + "description": "K\u00e9rj\u00fck, k\u00f6vesse ezt a [link] ({authorization_url} Author_url}) \u00e9s ** Enged\u00e9lyezze ** a hozz\u00e1f\u00e9r\u00e9st Ambiclimate -fi\u00f3kj\u00e1hoz, majd t\u00e9rjen vissza, \u00e9s nyomja meg az al\u00e1bbi ** K\u00fcld\u00e9s ** gombot.\n (Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a megadott visszah\u00edv\u00e1si URL {cb_url})", + "title": "Ambiclimate hiteles\u00edt\u00e9se" + } } } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/hu.json b/homeassistant/components/arcam_fmj/translations/hu.json index e1784c4ad66..dfccbbe7143 100644 --- a/homeassistant/components/arcam_fmj/translations/hu.json +++ b/homeassistant/components/arcam_fmj/translations/hu.json @@ -5,13 +5,21 @@ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, + "error": { + "one": "\u00dcres", + "other": "\u00dcres" + }, "flow_title": "{host}", "step": { + "confirm": { + "description": "Hozz\u00e1 szeretn\u00e9 adni az Arcam FMJ \"{host}\" eszk\u00f6zt a HomeAssistanthoz?" + }, "user": { "data": { "host": "Hoszt", "port": "Port" - } + }, + "description": "K\u00e9rj\u00fck, adja meg az eszk\u00f6z gazdag\u00e9pnev\u00e9t vagy IP-c\u00edm\u00e9t." } } } diff --git a/homeassistant/components/atag/translations/hu.json b/homeassistant/components/atag/translations/hu.json index 134f3bedfe8..8c3b4a055b0 100644 --- a/homeassistant/components/atag/translations/hu.json +++ b/homeassistant/components/atag/translations/hu.json @@ -4,14 +4,16 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unauthorized": "A p\u00e1ros\u00edt\u00e1s megtagadva, ellen\u0151rizze az eszk\u00f6z hiteles\u00edt\u00e9si k\u00e9r\u00e9s\u00e9t" }, "step": { "user": { "data": { "host": "Hoszt", "port": "Port" - } + }, + "title": "Csatlakoz\u00e1s az eszk\u00f6zh\u00f6z" } } } diff --git a/homeassistant/components/august/translations/hu.json b/homeassistant/components/august/translations/hu.json index fec6ad93b26..aeaef514e71 100644 --- a/homeassistant/components/august/translations/hu.json +++ b/homeassistant/components/august/translations/hu.json @@ -30,6 +30,7 @@ "data": { "code": "Ellen\u0151rz\u0151 k\u00f3d" }, + "description": "K\u00e9rj\u00fck, ellen\u0151rizze a {login_method} ({username}), \u00e9s \u00edrja be al\u00e1bb az ellen\u0151rz\u0151 k\u00f3dot", "title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s" } } diff --git a/homeassistant/components/awair/translations/hu.json b/homeassistant/components/awair/translations/hu.json index 53827adf344..f465186a95b 100644 --- a/homeassistant/components/awair/translations/hu.json +++ b/homeassistant/components/awair/translations/hu.json @@ -21,7 +21,8 @@ "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", "email": "E-mail" - } + }, + "description": "Regisztr\u00e1lnia kell az Awair fejleszt\u0151i hozz\u00e1f\u00e9r\u00e9si tokenj\u00e9hez a k\u00f6vetkez\u0151 c\u00edmen: https://developer.getawair.com/onboard/login" } } } diff --git a/homeassistant/components/axis/translations/hu.json b/homeassistant/components/axis/translations/hu.json index 972690ede97..709de5851ad 100644 --- a/homeassistant/components/axis/translations/hu.json +++ b/homeassistant/components/axis/translations/hu.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "link_local_address": "A linkek helyi c\u00edmei nem t\u00e1mogatottak", + "not_axis_device": "A felfedezett eszk\u00f6z nem Axis eszk\u00f6z" }, "error": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", @@ -17,7 +19,8 @@ "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "\u00c1ll\u00edtsa be az Axis eszk\u00f6zt" } } }, @@ -26,7 +29,8 @@ "configure_stream": { "data": { "stream_profile": "V\u00e1lassza ki a haszn\u00e1lni k\u00edv\u00e1nt adatfolyam-profilt" - } + }, + "title": "Axis eszk\u00f6z vide\u00f3 stream opci\u00f3k" } } } diff --git a/homeassistant/components/azure_devops/translations/hu.json b/homeassistant/components/azure_devops/translations/hu.json index f85c6795fd5..e42ebc8d8e2 100644 --- a/homeassistant/components/azure_devops/translations/hu.json +++ b/homeassistant/components/azure_devops/translations/hu.json @@ -6,14 +6,25 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "project_error": "Nem siker\u00fclt lek\u00e9rni a projekt adatait." }, "flow_title": "{project_url}", "step": { "reauth": { - "description": "A(z) {project_url} hiteles\u00edt\u00e9se nem siker\u00fclt. K\u00e9rj\u00fck, adja meg jelenlegi hiteles\u00edt\u0151 adatait." + "data": { + "personal_access_token": "Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si token (PAT)" + }, + "description": "A(z) {project_url} hiteles\u00edt\u00e9se nem siker\u00fclt. K\u00e9rj\u00fck, adja meg jelenlegi hiteles\u00edt\u0151 adatait.", + "title": "\u00dajrahiteles\u00edt\u00e9s" }, "user": { + "data": { + "organization": "Szervezet", + "personal_access_token": "Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si token (PAT)", + "project": "Projekt" + }, + "description": "\u00c1ll\u00edtson be egy Azure DevOps-p\u00e9ld\u00e1nyt a projekt el\u00e9r\u00e9s\u00e9hez. Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si token csak mag\u00e1nprojekthez sz\u00fcks\u00e9ges.", "title": "Azure DevOps Project hozz\u00e1ad\u00e1sa" } } diff --git a/homeassistant/components/blebox/translations/hu.json b/homeassistant/components/blebox/translations/hu.json index 97a6c1bdc18..ce51a8a0967 100644 --- a/homeassistant/components/blebox/translations/hu.json +++ b/homeassistant/components/blebox/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "address_already_configured": "Egy BleBox-eszk\u00f6z m\u00e1r konfigur\u00e1lva van a(z) {address} c\u00edmen.", "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { @@ -14,7 +15,9 @@ "data": { "host": "IP c\u00edm", "port": "Port" - } + }, + "description": "\u00c1ll\u00edtsa be a BleBox k\u00e9sz\u00fcl\u00e9ket a Homeassistantba val\u00f3 integr\u00e1ci\u00f3hoz.", + "title": "\u00c1ll\u00edtsa be a BleBox eszk\u00f6zt" } } } diff --git a/homeassistant/components/blink/translations/hu.json b/homeassistant/components/blink/translations/hu.json index e56b142a5b0..135a2f7ef2e 100644 --- a/homeassistant/components/blink/translations/hu.json +++ b/homeassistant/components/blink/translations/hu.json @@ -21,7 +21,19 @@ "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "Jelentkezzen be Blink-fi\u00f3kkal" + } + } + }, + "options": { + "step": { + "simple_options": { + "data": { + "scan_interval": "Szkennel\u00e9si intervallum (m\u00e1sodperc)" + }, + "description": "Blink integr\u00e1ci\u00f3 konfigur\u00e1l\u00e1sa", + "title": "Villog\u00e1si lehet\u0151s\u00e9gek" } } } diff --git a/homeassistant/components/braviatv/translations/hu.json b/homeassistant/components/braviatv/translations/hu.json index fbb23fdee04..5f96af8bad7 100644 --- a/homeassistant/components/braviatv/translations/hu.json +++ b/homeassistant/components/braviatv/translations/hu.json @@ -14,12 +14,14 @@ "data": { "pin": "PIN-k\u00f3d" }, + "description": "\u00cdrja be a Sony Bravia TV -n l\u00e1that\u00f3 PIN -k\u00f3dot. \n\n Ha a PIN -k\u00f3d nem jelenik meg, t\u00f6r\u00f6lje a Home Assistant regisztr\u00e1ci\u00f3j\u00e1t a t\u00e9v\u00e9n, l\u00e9pjen a k\u00f6vetkez\u0151re: Be\u00e1ll\u00edt\u00e1sok - > H\u00e1l\u00f3zat - > T\u00e1voli eszk\u00f6z be\u00e1ll\u00edt\u00e1sai - > T\u00e1vol\u00edtsa el a t\u00e1voli eszk\u00f6z regisztr\u00e1ci\u00f3j\u00e1t.", "title": "Sony Bravia TV enged\u00e9lyez\u00e9se" }, "user": { "data": { "host": "Hoszt" }, + "description": "\u00c1ll\u00edtsa be a Sony Bravia TV integr\u00e1ci\u00f3t. Ha probl\u00e9m\u00e1i vannak a konfigur\u00e1ci\u00f3val, l\u00e1togasson el a k\u00f6vetkez\u0151 oldalra: https://www.home-assistant.io/integrations/braviatv \n\n Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a TV be van kapcsolva.", "title": "Sony Bravia TV" } } diff --git a/homeassistant/components/bsblan/translations/hu.json b/homeassistant/components/bsblan/translations/hu.json index 499a7d92331..51feb8b75d7 100644 --- a/homeassistant/components/bsblan/translations/hu.json +++ b/homeassistant/components/bsblan/translations/hu.json @@ -11,10 +11,13 @@ "user": { "data": { "host": "Hoszt", + "passkey": "Jelsz\u00f3 karakterl\u00e1nc", "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "\u00c1ll\u00edtsa be a BSB-Lan eszk\u00f6zt az HomeAssistantba val\u00f3 integr\u00e1ci\u00f3hoz.", + "title": "Csatlakoz\u00e1s a BSB-Lan eszk\u00f6zh\u00f6z" } } } diff --git a/homeassistant/components/cert_expiry/translations/hu.json b/homeassistant/components/cert_expiry/translations/hu.json index 2ae516565e3..de459c324df 100644 --- a/homeassistant/components/cert_expiry/translations/hu.json +++ b/homeassistant/components/cert_expiry/translations/hu.json @@ -4,14 +4,21 @@ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", "import_failed": "Nem siker\u00fclt import\u00e1lni a konfigur\u00e1ci\u00f3t" }, + "error": { + "connection_refused": "A kapcsolat megtagadva a gazdag\u00e9phez val\u00f3 csatlakoz\u00e1skor", + "connection_timeout": "T\u00fall\u00e9p\u00e9s, amikor ehhez a gazdag\u00e9phez kapcsol\u00f3dik", + "resolve_failed": "Ez a gazdag\u00e9p nem oldhat\u00f3 fel" + }, "step": { "user": { "data": { "host": "Hoszt", "name": "A tan\u00fas\u00edtv\u00e1ny neve", "port": "Port" - } + }, + "title": "Hat\u00e1rozza meg a vizsg\u00e1land\u00f3 tan\u00fas\u00edtv\u00e1nyt" } } - } + }, + "title": "Tan\u00fas\u00edtv\u00e1ny lej\u00e1rata" } \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/hu.json b/homeassistant/components/co2signal/translations/hu.json new file mode 100644 index 00000000000..00bc19e7b49 --- /dev/null +++ b/homeassistant/components/co2signal/translations/hu.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "api_ratelimit": "API D\u00edjkorl\u00e1t t\u00fall\u00e9pve", + "unknown": "V\u00e1ratlan hiba" + }, + "error": { + "api_ratelimit": "API D\u00edjkorl\u00e1t t\u00fall\u00e9pve", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g" + } + }, + "country": { + "data": { + "country_code": "Orsz\u00e1g k\u00f3d" + } + }, + "user": { + "data": { + "api_key": "Hozz\u00e1f\u00e9r\u00e9si token", + "location": "Adatok lek\u00e9rdez\u00e9se a" + }, + "description": "Token k\u00e9r\u00e9s\u00e9hez l\u00e1togasson el a https://co2signal.com/ webhelyre." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/control4/translations/hu.json b/homeassistant/components/control4/translations/hu.json index 68cb4fe23a9..5d41eb09a84 100644 --- a/homeassistant/components/control4/translations/hu.json +++ b/homeassistant/components/control4/translations/hu.json @@ -14,6 +14,16 @@ "host": "IP c\u00edm", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "K\u00e9rj\u00fck, adja meg Control4-fi\u00f3kj\u00e1nak adatait \u00e9s a helyi vez\u00e9rl\u0151 IP-c\u00edm\u00e9t." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Friss\u00edt\u00e9sek k\u00f6z\u00f6tti m\u00e1sodpercek" } } } diff --git a/homeassistant/components/coolmaster/translations/hu.json b/homeassistant/components/coolmaster/translations/hu.json index bf67763ca6b..d52dba6b4b4 100644 --- a/homeassistant/components/coolmaster/translations/hu.json +++ b/homeassistant/components/coolmaster/translations/hu.json @@ -1,14 +1,21 @@ { "config": { "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "no_units": "Nem tal\u00e1lhat\u00f3 HVAC egys\u00e9g a CoolMasterNet gazdag\u00e9pben." }, "step": { "user": { "data": { + "cool": "T\u00e1mogatott a h\u0171t\u00e9si m\u00f3d(ok)", + "dry": "T\u00e1mogassa a p\u00e1r\u00e1tlan\u00edt\u00f3 m\u00f3d(ok)", + "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)", "host": "Hoszt", "off": "Ki lehet kapcsolni" - } + }, + "title": "\u00c1ll\u00edtsa be a CoolMasterNet kapcsolat r\u00e9szleteit." } } } diff --git a/homeassistant/components/deconz/translations/hu.json b/homeassistant/components/deconz/translations/hu.json index 84493ccb9f6..bc003a279e8 100644 --- a/homeassistant/components/deconz/translations/hu.json +++ b/homeassistant/components/deconz/translations/hu.json @@ -26,12 +26,18 @@ "host": "Hoszt", "port": "Port" } + }, + "user": { + "data": { + "host": "V\u00e1lassza ki a felfedezett deCONZ \u00e1tj\u00e1r\u00f3t" + } } } }, "device_automation": { "trigger_subtype": { "both_buttons": "Mindk\u00e9t gomb", + "bottom_buttons": "Als\u00f3 gombok", "button_1": "Els\u0151 gomb", "button_2": "M\u00e1sodik gomb", "button_3": "Harmadik gomb", @@ -52,6 +58,7 @@ "side_4": "4. oldal", "side_5": "5. oldal", "side_6": "6. oldal", + "top_buttons": "Fels\u0151 gombok", "turn_off": "Kikapcsolva", "turn_on": "Bekapcsolva" }, @@ -63,6 +70,7 @@ "remote_button_quadruple_press": "\"{subtype}\" gombra n\u00e9gyszer kattintottak", "remote_button_quintuple_press": "\"{subtype}\" gombra \u00f6tsz\u00f6r kattintottak", "remote_button_rotated": "A gomb elforgatva: \"{subtype}\"", + "remote_button_rotated_fast": "A gomb gyorsan elfordult: \"{subtype}\"", "remote_button_rotation_stopped": "A (z) \"{subtype}\" gomb forg\u00e1sa le\u00e1llt", "remote_button_short_press": "\"{subtype}\" gomb lenyomva", "remote_button_short_release": "\"{subtype}\" gomb elengedve", @@ -93,7 +101,8 @@ "allow_deconz_groups": "DeCONZ f\u00e9nycsoportok enged\u00e9lyez\u00e9se", "allow_new_devices": "Enged\u00e9lyezze az \u00faj eszk\u00f6z\u00f6k automatikus hozz\u00e1ad\u00e1s\u00e1t" }, - "description": "A deCONZ eszk\u00f6zt\u00edpusok l\u00e1that\u00f3s\u00e1g\u00e1nak konfigur\u00e1l\u00e1sa" + "description": "A deCONZ eszk\u00f6zt\u00edpusok l\u00e1that\u00f3s\u00e1g\u00e1nak konfigur\u00e1l\u00e1sa", + "title": "deCONZ opci\u00f3k" } } } diff --git a/homeassistant/components/demo/translations/hu.json b/homeassistant/components/demo/translations/hu.json index 0f8f1673d43..3bfe095189a 100644 --- a/homeassistant/components/demo/translations/hu.json +++ b/homeassistant/components/demo/translations/hu.json @@ -1,9 +1,16 @@ { "options": { "step": { + "init": { + "data": { + "one": "\u00dcres", + "other": "\u00dcres" + } + }, "options_1": { "data": { "bool": "Opcion\u00e1lis logikai \u00e9rt\u00e9k", + "constant": "\u00c1lland\u00f3", "int": "Numerikus bemenet" } }, diff --git a/homeassistant/components/denonavr/translations/hu.json b/homeassistant/components/denonavr/translations/hu.json index e6727d3c29f..43ee362d65a 100644 --- a/homeassistant/components/denonavr/translations/hu.json +++ b/homeassistant/components/denonavr/translations/hu.json @@ -3,17 +3,32 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", - "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra. A h\u00e1l\u00f3zati \u00e9s Ethernet k\u00e1belek kih\u00faz\u00e1sa \u00e9s \u00fajracsatlakoztat\u00e1sa seg\u00edthet" + "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra. A h\u00e1l\u00f3zati \u00e9s Ethernet k\u00e1belek kih\u00faz\u00e1sa \u00e9s \u00fajracsatlakoztat\u00e1sa seg\u00edthet", + "not_denonavr_manufacturer": "Nem egy Denon AVR h\u00e1l\u00f3zati vev\u0151, felfedezett gy\u00e1rt\u00f3 nem egyezik", + "not_denonavr_missing": "Nem Denon AVR h\u00e1l\u00f3zati vev\u0151, a felfedez\u00e9si inform\u00e1ci\u00f3k nem teljesek" }, "error": { "discovery_error": "Nem siker\u00fclt megtal\u00e1lni a Denon AVR h\u00e1l\u00f3zati er\u0151s\u00edt\u0151t" }, "flow_title": "{name}", "step": { + "confirm": { + "description": "K\u00e9rj\u00fck, er\u0151s\u00edtse meg a vev\u0151 hozz\u00e1ad\u00e1s\u00e1t", + "title": "Denon AVR h\u00e1l\u00f3zati vev\u0151k\u00e9sz\u00fcl\u00e9kek" + }, + "select": { + "data": { + "select_host": "Vev\u0151 IP-c\u00edme" + }, + "description": "Futtassa \u00fajra a be\u00e1ll\u00edt\u00e1st, ha tov\u00e1bbi vev\u0151k\u00e9sz\u00fcl\u00e9keket szeretne csatlakoztatni", + "title": "V\u00e1lassza ki a csatlakoztatni k\u00edv\u00e1nt vev\u0151t" + }, "user": { "data": { "host": "IP c\u00edm" - } + }, + "description": "Csatlakozzon a vev\u0151h\u00f6z, ha az IP-c\u00edm nincs be\u00e1ll\u00edtva, az automatikus felder\u00edt\u00e9st haszn\u00e1lja", + "title": "Denon AVR h\u00e1l\u00f3zati vev\u0151k\u00e9sz\u00fcl\u00e9kek" } } }, @@ -21,8 +36,13 @@ "step": { "init": { "data": { - "update_audyssey": "Friss\u00edtse az Audyssey be\u00e1ll\u00edt\u00e1sait" - } + "show_all_sources": "Az \u00f6sszes forr\u00e1s megjelen\u00edt\u00e9se", + "update_audyssey": "Friss\u00edtse az Audyssey be\u00e1ll\u00edt\u00e1sait", + "zone2": "\u00c1ll\u00edtsa be a 2. z\u00f3n\u00e1t", + "zone3": "\u00c1ll\u00edtsa be a 3. z\u00f3n\u00e1t" + }, + "description": "Adja meg az opcion\u00e1lis be\u00e1ll\u00edt\u00e1sokat", + "title": "Denon AVR h\u00e1l\u00f3zati vev\u0151k\u00e9sz\u00fcl\u00e9kek" } } } diff --git a/homeassistant/components/dexcom/translations/hu.json b/homeassistant/components/dexcom/translations/hu.json index 45f38b22a84..039eb56f8f0 100644 --- a/homeassistant/components/dexcom/translations/hu.json +++ b/homeassistant/components/dexcom/translations/hu.json @@ -14,7 +14,9 @@ "password": "Jelsz\u00f3", "server": "Szerver", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "Adja meg a Dexcom Share hiteles\u00edt\u0151 adatait", + "title": "Dexcom integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sa" } } }, diff --git a/homeassistant/components/directv/translations/hu.json b/homeassistant/components/directv/translations/hu.json index 0309eb35881..3e0a7d5cb57 100644 --- a/homeassistant/components/directv/translations/hu.json +++ b/homeassistant/components/directv/translations/hu.json @@ -7,7 +7,15 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, + "flow_title": "{name}", "step": { + "ssdp_confirm": { + "data": { + "one": "\u00dcres", + "other": "\u00dcres" + }, + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}-t?" + }, "user": { "data": { "host": "Hoszt" diff --git a/homeassistant/components/doorbird/translations/hu.json b/homeassistant/components/doorbird/translations/hu.json index 3f74783b7ac..cb4c46e699a 100644 --- a/homeassistant/components/doorbird/translations/hu.json +++ b/homeassistant/components/doorbird/translations/hu.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "link_local_address": "A linkek helyi c\u00edmei nem t\u00e1mogatottak", + "not_doorbird_device": "Ez az eszk\u00f6z nem DoorBird" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -16,7 +18,18 @@ "name": "Eszk\u00f6z neve", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "Csatlakozzon a DoorBird-hez" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "events": "Vessz\u0151vel elv\u00e1lasztott esem\u00e9nyek list\u00e1ja." + }, + "description": "Adjon hozz\u00e1 vessz\u0151vel elv\u00e1lasztott esem\u00e9nynevet minden k\u00f6vetni k\u00edv\u00e1nt esem\u00e9nyhez. Miut\u00e1n itt megadta \u0151ket, haszn\u00e1lja a DoorBird alkalmaz\u00e1st, hogy hozz\u00e1rendelje \u0151ket egy adott esem\u00e9nyhez. Tekintse meg a dokument\u00e1ci\u00f3t a https://www.home-assistant.io/integrations/doorbird/#events c\u00edmen. P\u00e9lda: valaki_pr\u00e9selt_gomb, mozg\u00e1s" } } } diff --git a/homeassistant/components/dunehd/translations/hu.json b/homeassistant/components/dunehd/translations/hu.json index cf0b593d546..148a6fde0d0 100644 --- a/homeassistant/components/dunehd/translations/hu.json +++ b/homeassistant/components/dunehd/translations/hu.json @@ -13,6 +13,7 @@ "data": { "host": "Hoszt" }, + "description": "\u00c1ll\u00edtsa be a Dune HD integr\u00e1ci\u00f3t. Ha probl\u00e9m\u00e1i vannak a konfigur\u00e1ci\u00f3val, l\u00e1togasson el a k\u00f6vetkez\u0151 oldalra: https://www.home-assistant.io/integrations/dunehd \n\n Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a lej\u00e1tsz\u00f3 be van kapcsolva.", "title": "Dune HD" } } diff --git a/homeassistant/components/eafm/translations/hu.json b/homeassistant/components/eafm/translations/hu.json index 38863029f12..820958e4e6e 100644 --- a/homeassistant/components/eafm/translations/hu.json +++ b/homeassistant/components/eafm/translations/hu.json @@ -1,13 +1,16 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "no_stations": "Nem tal\u00e1lhat\u00f3 \u00e1rv\u00edzfigyel\u0151 \u00e1llom\u00e1s." }, "step": { "user": { "data": { "station": "\u00c1llom\u00e1s" - } + }, + "description": "V\u00e1lassza ki a figyelni k\u00edv\u00e1nt \u00e1llom\u00e1st", + "title": "\u00c1rv\u00edzfigyel\u0151 \u00e1llom\u00e1s nyomon k\u00f6vet\u00e9se" } } } diff --git a/homeassistant/components/elgato/translations/hu.json b/homeassistant/components/elgato/translations/hu.json index ef6404bd92d..0cd9f2589b8 100644 --- a/homeassistant/components/elgato/translations/hu.json +++ b/homeassistant/components/elgato/translations/hu.json @@ -13,7 +13,12 @@ "data": { "host": "Hoszt", "port": "Port" - } + }, + "description": "\u00c1ll\u00edtsa be az Elgato Light-ot, hogy integr\u00e1lhat\u00f3 legyen az HomeAssistantba." + }, + "zeroconf_confirm": { + "description": "Hozz\u00e1 szeretn\u00e9 adni a \"{serial_number}\" sorozatsz\u00e1m\u00fa Elgato Light-ot az HomeAssistanthoz?", + "title": "Felfedezett Elgato Light eszk\u00f6z(\u00f6k)" } } } diff --git a/homeassistant/components/elkm1/translations/hu.json b/homeassistant/components/elkm1/translations/hu.json index 83862dfb75f..ff6445f0b72 100644 --- a/homeassistant/components/elkm1/translations/hu.json +++ b/homeassistant/components/elkm1/translations/hu.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "address_already_configured": "Az ElkM1 ezzel a c\u00edmmel m\u00e1r konfigur\u00e1lva van", + "already_configured": "Az ezzel az el\u0151taggal rendelkez\u0151 ElkM1 m\u00e1r konfigur\u00e1lva van" + }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", @@ -8,10 +12,15 @@ "step": { "user": { "data": { + "address": "Az IP-c\u00edm vagy tartom\u00e1ny vagy soros port, ha soros kapcsolaton kereszt\u00fcl csatlakozik.", "password": "Jelsz\u00f3", + "prefix": "Egyedi el\u0151tag (hagyja \u00fcresen, ha csak egy ElkM1 van).", "protocol": "Protokoll", + "temperature_unit": "Az ElkM1 h\u0151m\u00e9rs\u00e9kleti egys\u00e9g haszn\u00e1lja.", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "A c\u00edmsornak a \u201ebiztons\u00e1gos\u201d \u00e9s a \u201enem biztons\u00e1gos\u201d \u201ec\u00edm [: port]\u201d form\u00e1tum\u00fanak kell lennie. P\u00e9lda: '192.168.1.1'. A port opcion\u00e1lis, \u00e9s alap\u00e9rtelmez\u00e9s szerint 2101 \u201enem biztons\u00e1gos\u201d \u00e9s 2601 \u201ebiztons\u00e1gos\u201d. A soros protokollhoz a c\u00edmnek 'tty [: baud]' form\u00e1tum\u00fanak kell lennie. P\u00e9lda: '/dev/ttyS1'. A baud opcion\u00e1lis, \u00e9s alap\u00e9rtelmez\u00e9s szerint 115200.", + "title": "Csatlakoz\u00e1s az Elk-M1 vez\u00e9rl\u0151h\u00f6z" } } } diff --git a/homeassistant/components/energy/translations/hu.json b/homeassistant/components/energy/translations/hu.json new file mode 100644 index 00000000000..c8d85790fdd --- /dev/null +++ b/homeassistant/components/energy/translations/hu.json @@ -0,0 +1,3 @@ +{ + "title": "Energia" +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/hu.json b/homeassistant/components/enocean/translations/hu.json index 065747fb39d..9cc6843682c 100644 --- a/homeassistant/components/enocean/translations/hu.json +++ b/homeassistant/components/enocean/translations/hu.json @@ -1,7 +1,23 @@ { "config": { "abort": { + "invalid_dongle_path": "\u00c9rv\u00e9nytelen dongle \u00fatvonal", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "invalid_dongle_path": "Nem tal\u00e1lhat\u00f3 \u00e9rv\u00e9nyes dongle ehhez az \u00fatvonalhoz" + }, + "step": { + "detect": { + "data": { + "path": "USB dongle el\u00e9r\u00e9si \u00fatja" + } + }, + "manual": { + "data": { + "path": "USB dongle el\u00e9r\u00e9si \u00fatja" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/firmata/translations/hu.json b/homeassistant/components/firmata/translations/hu.json index 563ede56155..8224d177a9f 100644 --- a/homeassistant/components/firmata/translations/hu.json +++ b/homeassistant/components/firmata/translations/hu.json @@ -2,6 +2,10 @@ "config": { "abort": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "one": "\u00dcres", + "other": "\u00dcres" } } } \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/hu.json b/homeassistant/components/flick_electric/translations/hu.json index f7ed726e433..90ea92089e1 100644 --- a/homeassistant/components/flick_electric/translations/hu.json +++ b/homeassistant/components/flick_electric/translations/hu.json @@ -11,6 +11,8 @@ "step": { "user": { "data": { + "client_id": "Kliens ID (opcion\u00e1lis)", + "client_secret": "Kliens jelsz\u00f3 (nem k\u00f6telez\u0151)", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, diff --git a/homeassistant/components/flipr/translations/hu.json b/homeassistant/components/flipr/translations/hu.json new file mode 100644 index 00000000000..4daf0446abc --- /dev/null +++ b/homeassistant/components/flipr/translations/hu.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "no_flipr_id_found": "A fi\u00f3kj\u00e1hoz jelenleg nem tartozik Flipr-azonos\u00edt\u00f3. El\u0151sz\u00f6r ellen\u0151riznie kell, hogy m\u0171k\u00f6dik-e a Flipr mobilalkalmaz\u00e1s\u00e1val.", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "flipr_id": { + "data": { + "flipr_id": "Flipr azonos\u00edt\u00f3" + }, + "description": "V\u00e1lassza ki a Flipr azonos\u00edt\u00f3j\u00e1t a list\u00e1b\u00f3l", + "title": "V\u00e1lassza ki a Flipr-t" + }, + "user": { + "data": { + "email": "Email", + "password": "Jelsz\u00f3" + }, + "description": "Csatlakozzon a Flipr-fi\u00f3kj\u00e1val.", + "title": "Csatlakoz\u00e1s a Flipr-hez" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/hu.json b/homeassistant/components/flume/translations/hu.json index e607ac4255e..e1780be5654 100644 --- a/homeassistant/components/flume/translations/hu.json +++ b/homeassistant/components/flume/translations/hu.json @@ -19,9 +19,13 @@ }, "user": { "data": { + "client_id": "\u00dcgyf\u00e9lazonos\u00edt\u00f3", + "client_secret": "\u00dcgyf\u00e9l jelszva", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "A Flume Personal API el\u00e9r\u00e9s\u00e9hez \u201e\u00dcgyf\u00e9l-azonos\u00edt\u00f3t\u201d \u00e9s \u201e\u00dcgyf\u00e9ltitkot\u201d kell k\u00e9rnie a https://portal.flumetech.com/settings#token c\u00edmen.", + "title": "Csatlakozzon a Flume-fi\u00f3kj\u00e1hoz" } } } diff --git a/homeassistant/components/flunearyou/translations/hu.json b/homeassistant/components/flunearyou/translations/hu.json index 4f8cca2a939..b9ef1712ced 100644 --- a/homeassistant/components/flunearyou/translations/hu.json +++ b/homeassistant/components/flunearyou/translations/hu.json @@ -11,7 +11,8 @@ "data": { "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g" - } + }, + "description": "Figyelje a felhaszn\u00e1l\u00f3alap\u00fa \u00e9s a CDC jelent\u00e9seket egy p\u00e1r koordin\u00e1t\u00e1ra." } } } diff --git a/homeassistant/components/forked_daapd/translations/hu.json b/homeassistant/components/forked_daapd/translations/hu.json index 3400984dcd6..aac95b2956a 100644 --- a/homeassistant/components/forked_daapd/translations/hu.json +++ b/homeassistant/components/forked_daapd/translations/hu.json @@ -1,17 +1,24 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "not_forked_daapd": "Az eszk\u00f6z nem forked-daapd kiszolg\u00e1l\u00f3." }, "error": { "forbidden": "Nem tud csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a forked-daapd h\u00e1l\u00f3zati enged\u00e9lyeket.", - "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "websocket_not_enabled": "forked-daapd szerver websocket nincs enged\u00e9lyezve.", + "wrong_host_or_port": "Nem tud csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a gazdag\u00e9pet \u00e9s a portot.", + "wrong_password": "Helytelen jelsz\u00f3." }, "flow_title": "{name} ({host})", "step": { "user": { "data": { - "host": "Hoszt" + "host": "Hoszt", + "name": "Megjelen\u00edt\u00e9si n\u00e9v", + "password": "API jelsz\u00f3 (hagyja \u00fcresen, ha nincs jelsz\u00f3)", + "port": "API port" } } } diff --git a/homeassistant/components/freebox/translations/hu.json b/homeassistant/components/freebox/translations/hu.json index 1f0b848d3b6..c929d56f38e 100644 --- a/homeassistant/components/freebox/translations/hu.json +++ b/homeassistant/components/freebox/translations/hu.json @@ -9,6 +9,10 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "link": { + "description": "Kattintson a \u201eK\u00fcld\u00e9s\u201d gombra, majd \u00e9rintse meg a jobbra mutat\u00f3 nyilat az \u00fatv\u00e1laszt\u00f3n a Freebox regisztr\u00e1l\u00e1s\u00e1hoz a HomeAssistant seg\u00edts\u00e9g\u00e9vel. \n\n ! [A gomb helye az \u00fatv\u00e1laszt\u00f3n] (/static/images/config_freebox.png)", + "title": "Freebox \u00fatv\u00e1laszt\u00f3 linkel\u00e9se" + }, "user": { "data": { "host": "Hoszt", diff --git a/homeassistant/components/fritzbox/translations/hu.json b/homeassistant/components/fritzbox/translations/hu.json index 81639b1d830..50a81601310 100644 --- a/homeassistant/components/fritzbox/translations/hu.json +++ b/homeassistant/components/fritzbox/translations/hu.json @@ -4,6 +4,7 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "not_supported": "Csatlakoztatva az AVM FRITZ! Boxhoz, de nem tudja vez\u00e9relni az intelligens otthoni eszk\u00f6z\u00f6ket.", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" }, "error": { @@ -30,7 +31,8 @@ "host": "Hoszt", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "Adja meg az AVM FRITZ! Box adatait." } } } diff --git a/homeassistant/components/geonetnz_quakes/translations/hu.json b/homeassistant/components/geonetnz_quakes/translations/hu.json index 21a38c18e28..d6070db4fe7 100644 --- a/homeassistant/components/geonetnz_quakes/translations/hu.json +++ b/homeassistant/components/geonetnz_quakes/translations/hu.json @@ -6,6 +6,7 @@ "step": { "user": { "data": { + "mmi": "MMI", "radius": "Sug\u00e1r" }, "title": "T\u00f6ltsd ki a sz\u0171r\u0151 adatait." diff --git a/homeassistant/components/gogogate2/translations/hu.json b/homeassistant/components/gogogate2/translations/hu.json index 641046d7745..30d6ef5c016 100644 --- a/homeassistant/components/gogogate2/translations/hu.json +++ b/homeassistant/components/gogogate2/translations/hu.json @@ -15,6 +15,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, + "description": "Adja meg a sz\u00fcks\u00e9ges inform\u00e1ci\u00f3kat al\u00e1bb.", "title": "A GogoGate2 vagy az iSmartGate be\u00e1ll\u00edt\u00e1sa" } } diff --git a/homeassistant/components/growatt_server/translations/hu.json b/homeassistant/components/growatt_server/translations/hu.json index d856d13a96b..5b2efc737fe 100644 --- a/homeassistant/components/growatt_server/translations/hu.json +++ b/homeassistant/components/growatt_server/translations/hu.json @@ -17,6 +17,7 @@ "data": { "name": "N\u00e9v", "password": "Jelsz\u00f3", + "url": "URL", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, "title": "Adja meg Growatt adatait" diff --git a/homeassistant/components/guardian/translations/hu.json b/homeassistant/components/guardian/translations/hu.json index ca9a746f9d9..15469bead1e 100644 --- a/homeassistant/components/guardian/translations/hu.json +++ b/homeassistant/components/guardian/translations/hu.json @@ -13,7 +13,11 @@ "data": { "ip_address": "IP c\u00edm", "port": "Port" - } + }, + "description": "Konfigur\u00e1lja a helyi Elexa Guardian eszk\u00f6zt." + }, + "zeroconf_confirm": { + "description": "Be akarja \u00e1ll\u00edtani ezt a Guardian eszk\u00f6zt?" } } } diff --git a/homeassistant/components/hangouts/translations/hu.json b/homeassistant/components/hangouts/translations/hu.json index 3c065b01169..2f02ba9f623 100644 --- a/homeassistant/components/hangouts/translations/hu.json +++ b/homeassistant/components/hangouts/translations/hu.json @@ -19,6 +19,7 @@ }, "user": { "data": { + "authorization_code": "Enged\u00e9lyez\u00e9si k\u00f3d (k\u00e9zi hiteles\u00edt\u00e9shez sz\u00fcks\u00e9ges)", "email": "E-mail", "password": "Jelsz\u00f3" }, diff --git a/homeassistant/components/harmony/translations/hu.json b/homeassistant/components/harmony/translations/hu.json index a9cb6ccecee..4922bbd1ac6 100644 --- a/homeassistant/components/harmony/translations/hu.json +++ b/homeassistant/components/harmony/translations/hu.json @@ -7,11 +7,29 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, + "flow_title": "{name}", "step": { + "link": { + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}({host})-t?", + "title": "A Logitech Harmony Hub be\u00e1ll\u00edt\u00e1sa" + }, "user": { "data": { - "host": "Hoszt" - } + "host": "Hoszt", + "name": "Hub neve" + }, + "title": "A Logitech Harmony Hub be\u00e1ll\u00edt\u00e1sa" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "activity": "Az alap\u00e9rtelmezett tev\u00e9kenys\u00e9g, amelyet akkor kell v\u00e9grehajtani, ha nincs megadva.", + "delay_secs": "A parancsok k\u00fcld\u00e9se k\u00f6z\u00f6tti k\u00e9s\u00e9s." + }, + "description": "Harmony Hub be\u00e1ll\u00edt\u00e1sok" } } } diff --git a/homeassistant/components/heos/translations/hu.json b/homeassistant/components/heos/translations/hu.json index 2fbce1993cd..c487b49ee47 100644 --- a/homeassistant/components/heos/translations/hu.json +++ b/homeassistant/components/heos/translations/hu.json @@ -10,7 +10,9 @@ "user": { "data": { "host": "Hoszt" - } + }, + "description": "K\u00e9rj\u00fck, adja meg egy Heos-eszk\u00f6z gazdag\u00e9pnev\u00e9t vagy IP-c\u00edm\u00e9t (lehet\u0151leg egy vezet\u00e9kkel a h\u00e1l\u00f3zathoz csatlakoztatott eszk\u00f6zt).", + "title": "Csatlakoz\u00e1s a Heos-hoz" } } } diff --git a/homeassistant/components/homeassistant/translations/he.json b/homeassistant/components/homeassistant/translations/he.json index f86d7b0dca0..20de5a2d1b7 100644 --- a/homeassistant/components/homeassistant/translations/he.json +++ b/homeassistant/components/homeassistant/translations/he.json @@ -8,6 +8,7 @@ "os_version": "\u05d2\u05d9\u05e8\u05e1\u05ea \u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05e4\u05e2\u05dc\u05d4", "python_version": "\u05d2\u05e8\u05e1\u05ea \u05e4\u05d9\u05d9\u05ea\u05d5\u05df", "timezone": "\u05d0\u05d6\u05d5\u05e8 \u05d6\u05de\u05df", + "user": "\u05de\u05e9\u05ea\u05de\u05e9", "version": "\u05d2\u05d9\u05e8\u05e1\u05d4" } } diff --git a/homeassistant/components/homeassistant/translations/hu.json b/homeassistant/components/homeassistant/translations/hu.json index 9eddeeba112..b4da84596bf 100644 --- a/homeassistant/components/homeassistant/translations/hu.json +++ b/homeassistant/components/homeassistant/translations/hu.json @@ -10,6 +10,7 @@ "os_version": "Oper\u00e1ci\u00f3s rendszer verzi\u00f3ja", "python_version": "Python verzi\u00f3", "timezone": "Id\u0151z\u00f3na", + "user": "Felhaszn\u00e1l\u00f3", "version": "Verzi\u00f3", "virtualenv": "Virtu\u00e1lis k\u00f6rnyezet" } diff --git a/homeassistant/components/homekit/translations/hu.json b/homeassistant/components/homekit/translations/hu.json index 1afc0183a0d..c6fdf0afd74 100644 --- a/homeassistant/components/homekit/translations/hu.json +++ b/homeassistant/components/homekit/translations/hu.json @@ -1,13 +1,18 @@ { "config": { + "abort": { + "port_name_in_use": "Az azonos nev\u0171 vagy port\u00fa tartoz\u00e9k vagy h\u00edd m\u00e1r konfigur\u00e1lva van." + }, "step": { "pairing": { + "description": "A p\u00e1ros\u00edt\u00e1s befejez\u00e9s\u00e9hez k\u00f6vesse a \u201eHomeKit p\u00e1ros\u00edt\u00e1s\u201d szakasz \u201e\u00c9rtes\u00edt\u00e9sek\u201d szakasz\u00e1ban tal\u00e1lhat\u00f3 utas\u00edt\u00e1sokat.", "title": "HomeKit p\u00e1ros\u00edt\u00e1s" }, "user": { "data": { "include_domains": "Felvenni k\u00edv\u00e1nt domainek" }, + "description": "V\u00e1lassza ki a felvenni k\u00edv\u00e1nt domaineket. A domain minden t\u00e1mogatott entit\u00e1sa szerepelni fog. Minden tartoz\u00e9k m\u00f3dban k\u00fcl\u00f6n HomeKit p\u00e9ld\u00e1ny j\u00f6n l\u00e9tre minden TV m\u00e9dialej\u00e1tsz\u00f3hoz, tev\u00e9kenys\u00e9g alap\u00fa t\u00e1vir\u00e1ny\u00edt\u00f3hoz, z\u00e1rhoz \u00e9s f\u00e9nyk\u00e9pez\u0151g\u00e9phez.", "title": "Felvenni k\u00edv\u00e1nt domainek kiv\u00e1laszt\u00e1sa" } } @@ -15,12 +20,17 @@ "options": { "step": { "advanced": { + "data": { + "auto_start": "Automatikus ind\u00edt\u00e1s (tiltsa le, ha manu\u00e1lisan h\u00edvja a homekit.start szolg\u00e1ltat\u00e1st)" + }, + "description": "Ezeket a be\u00e1ll\u00edt\u00e1sokat csak akkor kell m\u00f3dos\u00edtani, ha a HomeKit nem m\u0171k\u00f6dik.", "title": "Halad\u00f3 be\u00e1ll\u00edt\u00e1sok" }, "cameras": { "data": { "camera_copy": "A nat\u00edv H.264 streameket t\u00e1mogat\u00f3 kamer\u00e1k" }, + "description": "Ellen\u0151rizze az \u00f6sszes kamer\u00e1t, amely t\u00e1mogatja a nat\u00edv H.264 adatfolyamokat. Ha a f\u00e9nyk\u00e9pez\u0151g\u00e9p nem ad ki H.264 adatfolyamot, a rendszer \u00e1tk\u00f3dolja a vide\u00f3t H.264 form\u00e1tumba a HomeKit sz\u00e1m\u00e1ra. Az \u00e1tk\u00f3dol\u00e1shoz nagy teljes\u00edtm\u00e9ny\u0171 CPU sz\u00fcks\u00e9ges, \u00e9s val\u00f3sz\u00edn\u0171leg nem fog m\u0171k\u00f6dni egylapos sz\u00e1m\u00edt\u00f3g\u00e9peken.", "title": "V\u00e1laszd ki a kamera vide\u00f3 kodekj\u00e9t." }, "include_exclude": { @@ -28,6 +38,7 @@ "entities": "Entit\u00e1sok", "mode": "M\u00f3d" }, + "description": "V\u00e1lassza ki a felvenni k\u00edv\u00e1nt entit\u00e1sokat. Kieg\u00e9sz\u00edt\u0151 m\u00f3dban csak egyetlen entit\u00e1s szerepel. H\u00eddbefogad\u00e1si m\u00f3dban a tartom\u00e1ny \u00f6sszes entit\u00e1sa szerepelni fog, hacsak nincsenek kijel\u00f6lve konkr\u00e9t entit\u00e1sok. H\u00eddkiz\u00e1r\u00e1si m\u00f3dban a domain \u00f6sszes entit\u00e1sa szerepelni fog, kiv\u00e9ve a kiz\u00e1rt entit\u00e1sokat. A legjobb teljes\u00edtm\u00e9ny \u00e9rdek\u00e9ben minden TV m\u00e9dialej\u00e1tsz\u00f3hoz, tev\u00e9kenys\u00e9galap\u00fa t\u00e1vir\u00e1ny\u00edt\u00f3hoz, z\u00e1rhoz \u00e9s f\u00e9nyk\u00e9pez\u0151g\u00e9phez k\u00fcl\u00f6n HomeKit tartoz\u00e9kot hoznak l\u00e9tre.", "title": "V\u00e1laszd ki a felvenni k\u00edv\u00e1nt entit\u00e1sokat" }, "init": { @@ -35,6 +46,7 @@ "include_domains": "Felvenni k\u00edv\u00e1nt domainek", "mode": "M\u00f3d" }, + "description": "A HomeKit konfigur\u00e1lhat\u00f3 \u00fagy, hogy egy h\u00edd vagy egyetlen tartoz\u00e9k l\u00e1that\u00f3 legyen. Kieg\u00e9sz\u00edt\u0151 m\u00f3dban csak egyetlen entit\u00e1s haszn\u00e1lhat\u00f3. A tartoz\u00e9k m\u00f3dra van sz\u00fcks\u00e9g ahhoz, hogy a TV -eszk\u00f6zoszt\u00e1ly\u00fa m\u00e9dialej\u00e1tsz\u00f3k megfelel\u0151en m\u0171k\u00f6djenek. A \u201eTartalmazand\u00f3 tartom\u00e1nyok\u201d entit\u00e1sai szerepelni fognak a HomeKitben. A k\u00f6vetkez\u0151 k\u00e9perny\u0151n kiv\u00e1laszthatja, hogy mely entit\u00e1sokat k\u00edv\u00e1nja felvenni vagy kiz\u00e1rni a list\u00e1b\u00f3l.", "title": "V\u00e1laszd ki a felvenni k\u00edv\u00e1nt domaineket." }, "yaml": { diff --git a/homeassistant/components/homekit_controller/translations/hu.json b/homeassistant/components/homekit_controller/translations/hu.json index cd06d12e809..1ad63bfb508 100644 --- a/homeassistant/components/homekit_controller/translations/hu.json +++ b/homeassistant/components/homekit_controller/translations/hu.json @@ -21,6 +21,7 @@ "flow_title": "HomeKit tartoz\u00e9k: {name}", "step": { "busy_error": { + "description": "Sz\u00fcntesse meg a p\u00e1ros\u00edt\u00e1st az \u00f6sszes vez\u00e9rl\u0151n, vagy pr\u00f3b\u00e1lja \u00fajraind\u00edtani az eszk\u00f6zt, majd folytassa a p\u00e1ros\u00edt\u00e1st.", "title": "Az eszk\u00f6z m\u00e1r p\u00e1rosul egy m\u00e1sik vez\u00e9rl\u0151vel" }, "max_tries_error": { @@ -36,6 +37,7 @@ "title": "HomeKit tartoz\u00e9k p\u00e1ros\u00edt\u00e1sa" }, "protocol_error": { + "description": "El\u0151fordulhat, hogy a k\u00e9sz\u00fcl\u00e9k nincs p\u00e1ros\u00edt\u00e1si m\u00f3dban, \u00e9s sz\u00fcks\u00e9g lehet fizikai vagy virtu\u00e1lis gombnyom\u00e1sra. Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy az eszk\u00f6z p\u00e1ros\u00edt\u00e1si m\u00f3dban van, vagy pr\u00f3b\u00e1lja \u00fajraind\u00edtani az eszk\u00f6zt, majd folytassa a p\u00e1ros\u00edt\u00e1st.", "title": "Hiba t\u00f6rt\u00e9nt a tartoz\u00e9kkal val\u00f3 kommunik\u00e1ci\u00f3 sor\u00e1n" }, "user": { diff --git a/homeassistant/components/honeywell/translations/hu.json b/homeassistant/components/honeywell/translations/hu.json new file mode 100644 index 00000000000..5583dc22f2e --- /dev/null +++ b/homeassistant/components/honeywell/translations/hu.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "K\u00e9rj\u00fck, adja meg a mytotalconnectcomfort.com webhelyre val\u00f3 bejelentkez\u00e9shez haszn\u00e1lt hiteles\u00edt\u0151 adatokat.", + "title": "Honeywell Total Connect Comfort (USA)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/hu.json b/homeassistant/components/huawei_lte/translations/hu.json index eff9c8a813b..22bd37c37ba 100644 --- a/homeassistant/components/huawei_lte/translations/hu.json +++ b/homeassistant/components/huawei_lte/translations/hu.json @@ -11,6 +11,8 @@ "incorrect_username": "Helytelen felhaszn\u00e1l\u00f3n\u00e9v", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_url": "\u00c9rv\u00e9nytelen URL", + "login_attempts_exceeded": "T\u00fall\u00e9pte a maxim\u00e1lis bejelentkez\u00e9si k\u00eds\u00e9rleteket. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra k\u00e9s\u0151bb", + "response_error": "Ismeretlen hiba az eszk\u00f6zr\u0151l", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "flow_title": "{name}", @@ -21,6 +23,7 @@ "url": "URL", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, + "description": "Adja meg az eszk\u00f6z hozz\u00e1f\u00e9r\u00e9si adatait.", "title": "Huawei LTE konfigur\u00e1l\u00e1sa" } } diff --git a/homeassistant/components/hue/translations/hu.json b/homeassistant/components/hue/translations/hu.json index d0aa043b10b..91321f9c6fd 100644 --- a/homeassistant/components/hue/translations/hu.json +++ b/homeassistant/components/hue/translations/hu.json @@ -35,12 +35,32 @@ }, "device_automation": { "trigger_subtype": { + "button_1": "Els\u0151 gomb", + "button_2": "M\u00e1sodik gomb", + "button_3": "Harmadik gomb", + "button_4": "Negyedik gomb", + "dim_down": "S\u00f6t\u00e9t\u00edt", + "dim_up": "Vil\u00e1gos\u00edt", + "double_buttons_1_3": "Els\u0151 \u00e9s harmadik gomb", + "double_buttons_2_4": "M\u00e1sodik \u00e9s negyedik gomb", "turn_off": "Kikapcsol\u00e1s", "turn_on": "Bekapcsol\u00e1s" }, "trigger_type": { + "remote_button_long_release": "A \"{subtype}\" gomb hossz\u00fa megnyom\u00e1s ut\u00e1n elengedve", "remote_button_short_press": "\"{subtype}\" gomb lenyomva", - "remote_button_short_release": "\"{subtype}\" gomb elengedve" + "remote_button_short_release": "\"{subtype}\" gomb elengedve", + "remote_double_button_long_press": "Mindk\u00e9t \"{subtype}\" hossz\u00fa megnyom\u00e1st k\u00f6vet\u0151en megjelent", + "remote_double_button_short_press": "Mindk\u00e9t \"{subtype}\" megjelent" + } + }, + "options": { + "step": { + "init": { + "data": { + "allow_hue_groups": "Hue csoportok enged\u00e9lyez\u00e9se" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/hu.json b/homeassistant/components/hunterdouglas_powerview/translations/hu.json index 3de1b9d0117..1fedd8bc126 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/hu.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/hu.json @@ -9,10 +9,15 @@ }, "flow_title": "{name} ({host})", "step": { + "link": { + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}({host})-t?", + "title": "Csatlakozzon a PowerView Hubhoz" + }, "user": { "data": { "host": "IP c\u00edm" - } + }, + "title": "Csatlakozzon a PowerView Hubhoz" } } } diff --git a/homeassistant/components/hvv_departures/translations/hu.json b/homeassistant/components/hvv_departures/translations/hu.json index deab9bcb929..dfbdd92f27a 100644 --- a/homeassistant/components/hvv_departures/translations/hu.json +++ b/homeassistant/components/hvv_departures/translations/hu.json @@ -5,15 +5,29 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "no_results": "Nincs eredm\u00e9ny. Pr\u00f3b\u00e1lja ki m\u00e1sik \u00e1llom\u00e1ssal/c\u00edmmel" }, "step": { + "station": { + "data": { + "station": "\u00c1llom\u00e1s/c\u00edm" + }, + "title": "Adja meg az \u00e1llom\u00e1st/c\u00edmet" + }, + "station_select": { + "data": { + "station": "\u00c1llom\u00e1s/c\u00edm" + }, + "title": "\u00c1llom\u00e1s/c\u00edm kiv\u00e1laszt\u00e1sa" + }, "user": { "data": { "host": "Hoszt", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "Csatlakozzon a HVV API-hoz" } } }, @@ -21,8 +35,11 @@ "step": { "init": { "data": { - "offset": "Eltol\u00e1s (perc)" + "filter": "V\u00e1lassza ki a sorokat", + "offset": "Eltol\u00e1s (perc)", + "real_time": "Val\u00f3s idej\u0171 adatok haszn\u00e1lata" }, + "description": "M\u00f3dos\u00edtsa az indul\u00e1si \u00e9rz\u00e9kel\u0151 be\u00e1ll\u00edt\u00e1sait", "title": "Be\u00e1ll\u00edt\u00e1sok" } } diff --git a/homeassistant/components/iaqualink/translations/hu.json b/homeassistant/components/iaqualink/translations/hu.json index dcb7b906ee3..1ca85c41190 100644 --- a/homeassistant/components/iaqualink/translations/hu.json +++ b/homeassistant/components/iaqualink/translations/hu.json @@ -11,7 +11,9 @@ "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "K\u00e9rj\u00fck, adja meg iAqualink-fi\u00f3kja felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t.", + "title": "Csatlakoz\u00e1s az iAqualinkhez" } } } diff --git a/homeassistant/components/icloud/translations/hu.json b/homeassistant/components/icloud/translations/hu.json index bb47cdd879b..722b3711e67 100644 --- a/homeassistant/components/icloud/translations/hu.json +++ b/homeassistant/components/icloud/translations/hu.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "no_device": "Egyik k\u00e9sz\u00fcl\u00e9ke sem aktiv\u00e1lta az \"iPhone keres\u00e9se\" funkci\u00f3t", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" }, "error": { @@ -14,6 +15,7 @@ "data": { "password": "Jelsz\u00f3" }, + "description": "A(z) {username} kor\u00e1bban megadott jelszava m\u00e1r nem m\u0171k\u00f6dik. Az integr\u00e1ci\u00f3 haszn\u00e1lat\u00e1hoz friss\u00edtse jelszav\u00e1t.", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, "trusted_device": { @@ -26,7 +28,8 @@ "user": { "data": { "password": "Jelsz\u00f3", - "username": "E-mail" + "username": "E-mail", + "with_family": "Csal\u00e1ddal" }, "description": "Adja meg hiteles\u00edt\u0151 adatait", "title": "iCloud hiteles\u00edt\u0151 adatok" diff --git a/homeassistant/components/insteon/translations/fa.json b/homeassistant/components/insteon/translations/fa.json new file mode 100644 index 00000000000..2456fbcba00 --- /dev/null +++ b/homeassistant/components/insteon/translations/fa.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0628\u0647 \u062f\u0631\u0633\u062a\u06cc \u062a\u0646\u0638\u06cc\u0645 \u0634\u062f\u0647 \u0627\u0633\u062a. \u062a\u0646\u0647\u0627 \u06cc\u06a9 \u062a\u0646\u0638\u06cc\u0645 \u0627\u0645\u06a9\u0627\u0646 \u067e\u0630\u06cc\u0631 \u0627\u0633\u062a." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/hu.json b/homeassistant/components/insteon/translations/hu.json index 462fae3e1cb..8444aa97655 100644 --- a/homeassistant/components/insteon/translations/hu.json +++ b/homeassistant/components/insteon/translations/hu.json @@ -31,6 +31,7 @@ "data": { "device": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" }, + "description": "Konfigur\u00e1lja az Insteon PowerLink modemet (PLM).", "title": "Insteon PLM" }, "user": { @@ -44,16 +45,28 @@ }, "options": { "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "input_error": "\u00c9rv\u00e9nytelen bejegyz\u00e9sek, ellen\u0151rizze \u00e9rt\u00e9keket.", + "select_single": "V\u00e1lassz egy lehet\u0151s\u00e9get" }, "step": { "add_override": { + "data": { + "address": "Eszk\u00f6z c\u00edme (azaz 1a2b3c)", + "cat": "Eszk\u00f6zkateg\u00f3ria (azaz 0x10)", + "subcat": "Eszk\u00f6z alkateg\u00f3ria (azaz 0x0a)" + }, + "description": "Eszk\u00f6z-fel\u00fclb\u00edr\u00e1l\u00e1s hozz\u00e1ad\u00e1sa.", "title": "Insteon" }, "add_x10": { "data": { + "housecode": "H\u00e1zk\u00f3d (a - p)", + "platform": "Platform", + "steps": "F\u00e9nyer\u0151-szab\u00e1lyoz\u00e1si l\u00e9p\u00e9sek (csak k\u00f6nny\u0171 eszk\u00f6z\u00f6k eset\u00e9n, alap\u00e9rtelmezett 22)", "unitcode": "Egys\u00e9gk\u00f3d (1 - 16)" }, + "description": "M\u00f3dos\u00edtsa az Insteon Hub jelszav\u00e1t.", "title": "Insteon" }, "change_hub_config": { @@ -63,15 +76,25 @@ "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, + "description": "M\u00f3dos\u00edtsa az Insteon Hub csatlakoz\u00e1si adatait. A m\u00f3dos\u00edt\u00e1s elv\u00e9gz\u00e9se ut\u00e1n \u00fajra kell ind\u00edtania a Home Assistant alkalmaz\u00e1st. Ez nem v\u00e1ltoztatja meg a Hub konfigur\u00e1ci\u00f3j\u00e1t. A Hub konfigur\u00e1ci\u00f3j\u00e1nak m\u00f3dos\u00edt\u00e1s\u00e1hoz haszn\u00e1lja a Hub alkalmaz\u00e1st.", "title": "Insteon" }, "init": { "data": { - "add_x10": "Adjon hozz\u00e1 egy X10 eszk\u00f6zt." + "add_override": "Eszk\u00f6z-fel\u00fclb\u00edr\u00e1l\u00e1s hozz\u00e1ad\u00e1sa.", + "add_x10": "Adjon hozz\u00e1 egy X10 eszk\u00f6zt.", + "change_hub_config": "M\u00f3dos\u00edtsa a Hub konfigur\u00e1ci\u00f3j\u00e1t.", + "remove_override": "Egy eszk\u00f6z fel\u00fclb\u00edr\u00e1lat\u00e1nak elt\u00e1vol\u00edt\u00e1sa.", + "remove_x10": "T\u00e1vol\u00edtson el egy X10 eszk\u00f6zt." }, + "description": "V\u00e1lasszon egy be\u00e1ll\u00edt\u00e1st.", "title": "Insteon" }, "remove_override": { + "data": { + "address": "V\u00e1lassza ki az elt\u00e1vol\u00edtani k\u00edv\u00e1nt eszk\u00f6z c\u00edm\u00e9t" + }, + "description": "T\u00e1vol\u00edtsa el az eszk\u00f6z fel\u00fclb\u00edr\u00e1l\u00e1s\u00e1t", "title": "Insteon" }, "remove_x10": { diff --git a/homeassistant/components/ipp/translations/hu.json b/homeassistant/components/ipp/translations/hu.json index 8c988eff551..a024cfb2e56 100644 --- a/homeassistant/components/ipp/translations/hu.json +++ b/homeassistant/components/ipp/translations/hu.json @@ -3,7 +3,11 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "connection_upgrade": "Nem siker\u00fclt csatlakozni a nyomtat\u00f3hoz, mert a kapcsolat friss\u00edt\u00e9se sz\u00fcks\u00e9ges." + "connection_upgrade": "Nem siker\u00fclt csatlakozni a nyomtat\u00f3hoz, mert a kapcsolat friss\u00edt\u00e9se sz\u00fcks\u00e9ges.", + "ipp_error": "IPP hiba t\u00f6rt\u00e9nt.", + "ipp_version_error": "A nyomtat\u00f3 nem t\u00e1mogatja az IPP verzi\u00f3t.", + "parse_error": "Nem siker\u00fclt elemezni a nyomtat\u00f3 v\u00e1lasz\u00e1t.", + "unique_id_required": "Az eszk\u00f6zb\u0151l hi\u00e1nyzik a felfedez\u00e9shez sz\u00fcks\u00e9ges egyedi azonos\u00edt\u00f3." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -13,14 +17,18 @@ "step": { "user": { "data": { + "base_path": "Relat\u00edv \u00fatvonal a nyomtat\u00f3hoz", "host": "Hoszt", "port": "Port", "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" - } + }, + "description": "\u00c1ll\u00edtsa be a nyomtat\u00f3t az Internet Printing Protocol (IPP) protokollon kereszt\u00fcl, hogy integr\u00e1lhat\u00f3 legyen a Home Assistant seg\u00edts\u00e9g\u00e9vel.", + "title": "Kapcsolja \u00f6ssze a nyomtat\u00f3t" }, "zeroconf_confirm": { - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}-t?" + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}-t?", + "title": "Felfedezett nyomtat\u00f3" } } } diff --git a/homeassistant/components/iqvia/translations/hu.json b/homeassistant/components/iqvia/translations/hu.json index f5301e874ea..0ae420e47aa 100644 --- a/homeassistant/components/iqvia/translations/hu.json +++ b/homeassistant/components/iqvia/translations/hu.json @@ -2,6 +2,18 @@ "config": { "abort": { "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "invalid_zip_code": "Az ir\u00e1ny\u00edt\u00f3sz\u00e1m \u00e9rv\u00e9nytelen" + }, + "step": { + "user": { + "data": { + "zip_code": "Ir\u00e1ny\u00edt\u00f3sz\u00e1m" + }, + "description": "T\u00f6ltse ki amerikai vagy kanadai ir\u00e1ny\u00edt\u00f3sz\u00e1m\u00e1t.", + "title": "IQVIA" + } } } } \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/translations/hu.json b/homeassistant/components/islamic_prayer_times/translations/hu.json index 065747fb39d..5bad8174b9a 100644 --- a/homeassistant/components/islamic_prayer_times/translations/hu.json +++ b/homeassistant/components/islamic_prayer_times/translations/hu.json @@ -2,6 +2,22 @@ "config": { "abort": { "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "user": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani az iszl\u00e1m imaid\u0151ket?", + "title": "\u00c1ll\u00edtsa be az iszl\u00e1m imaid\u0151t" + } } - } + }, + "options": { + "step": { + "init": { + "data": { + "calculation_method": "Az ima sz\u00e1m\u00edt\u00e1si m\u00f3dszere" + } + } + } + }, + "title": "Iszl\u00e1m ima id\u0151k" } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/hu.json b/homeassistant/components/isy994/translations/hu.json index 065be706d0f..dab85300e6d 100644 --- a/homeassistant/components/isy994/translations/hu.json +++ b/homeassistant/components/isy994/translations/hu.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_host": "A gazdag\u00e9p bejegyz\u00e9se nem volt teljes URL-form\u00e1tumban, p\u00e9ld\u00e1ul: http://192.168.10.100:80", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "flow_title": "{name} ({host})", @@ -14,14 +15,24 @@ "data": { "host": "URL", "password": "Jelsz\u00f3", + "tls": "Az ISY vez\u00e9rl\u0151 TLS verzi\u00f3ja.", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "A gazdag\u00e9p bejegyz\u00e9s\u00e9nek teljes URL form\u00e1tumban kell lennie, pl. Http://192.168.10.100:80", + "title": "Csatlakozzon az ISY994-hez" } } }, "options": { "step": { "init": { + "data": { + "ignore_string": "Figyelmen k\u00edv\u00fcl hagyja a karakterl\u00e1ncot", + "restore_light_state": "F\u00e9nyer\u0151 vissza\u00e1ll\u00edt\u00e1sa", + "sensor_string": "Csom\u00f3pont \u00e9rz\u00e9kel\u0151 karakterl\u00e1nc", + "variable_sensor_string": "V\u00e1ltoz\u00f3 \u00e9rz\u00e9kel\u0151 karakterl\u00e1nc" + }, + "description": "\u00c1ll\u00edtsa be az ISY integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sait:\n \u2022 Csom\u00f3pont -\u00e9rz\u00e9kel\u0151 karakterl\u00e1nc: B\u00e1rmely eszk\u00f6z vagy mappa, amelynek nev\u00e9ben \u201eNode Sensor String\u201d szerepel, \u00e9rz\u00e9kel\u0151k\u00e9nt vagy bin\u00e1ris \u00e9rz\u00e9kel\u0151k\u00e9nt fog kezelni.\n \u2022 Karakterl\u00e1nc figyelmen k\u00edv\u00fcl hagy\u00e1sa: Minden olyan eszk\u00f6z, amelynek a neve \u201eIgnore String\u201d, figyelmen k\u00edv\u00fcl marad.\n \u2022 V\u00e1ltoz\u00f3 \u00e9rz\u00e9kel\u0151 karakterl\u00e1nc: B\u00e1rmely v\u00e1ltoz\u00f3, amely tartalmazza a \u201eV\u00e1ltoz\u00f3 \u00e9rz\u00e9kel\u0151 karakterl\u00e1ncot\u201d, hozz\u00e1ad\u00f3dik \u00e9rz\u00e9kel\u0151k\u00e9nt.\n \u2022 F\u00e9nyer\u0151ss\u00e9g vissza\u00e1ll\u00edt\u00e1sa: Ha enged\u00e9lyezve van, akkor az el\u0151z\u0151 f\u00e9nyer\u0151 vissza\u00e1ll, amikor a k\u00e9sz\u00fcl\u00e9ket be\u00e9p\u00edtett On-Level helyett bekapcsolja.", "title": "ISY994 Be\u00e1ll\u00edt\u00e1sok" } } diff --git a/homeassistant/components/juicenet/translations/hu.json b/homeassistant/components/juicenet/translations/hu.json index f04a8c1e6ca..63e6086190b 100644 --- a/homeassistant/components/juicenet/translations/hu.json +++ b/homeassistant/components/juicenet/translations/hu.json @@ -13,6 +13,7 @@ "data": { "api_token": "API Token" }, + "description": "Sz\u00fcks\u00e9ge lesz az API Tokenre a https://home.juice.net/Manage webhelyen.", "title": "Csatlakoz\u00e1s a JuiceNethez" } } diff --git a/homeassistant/components/konnected/translations/hu.json b/homeassistant/components/konnected/translations/hu.json index 507e5d258f2..1ad58223b88 100644 --- a/homeassistant/components/konnected/translations/hu.json +++ b/homeassistant/components/konnected/translations/hu.json @@ -3,38 +3,107 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "not_konn_panel": "Nem felismert Konnected.io eszk\u00f6z", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { + "confirm": { + "description": "Modell: {model}\nAzonos\u00edt\u00f3: {id}\nGazdag\u00e9p: {host}\nPort: {port} \n\n Az IO \u00e9s a panel viselked\u00e9s\u00e9t a Konnected Alarm Panel be\u00e1ll\u00edt\u00e1saiban konfigur\u00e1lhatja.", + "title": "Konnected eszk\u00f6z k\u00e9sz" + }, + "import_confirm": { + "description": "A konfigur\u00e1ci\u00f3s.yaml f\u00e1jlban felfedezt\u00fcnk egy Konnected Alarm Panel-t {id} Ez a folyamat lehet\u0151v\u00e9 teszi, hogy import\u00e1lja azt egy konfigur\u00e1ci\u00f3s bejegyz\u00e9sbe.", + "title": "Konnected eszk\u00f6z import\u00e1l\u00e1sa" + }, "user": { "data": { "host": "IP c\u00edm", "port": "Port" - } + }, + "description": "K\u00e9rj\u00fck, adja meg a Konnected Panel gazdag\u00e9p\u00e9nek adatait." } } }, "options": { + "abort": { + "not_konn_panel": "Nem felismert Konnected.io eszk\u00f6z" + }, + "error": { + "bad_host": "\u00c9rv\u00e9nytelen fel\u00fclb\u00edr\u00e1l\u00e1si API host URL", + "one": "\u00dcres", + "other": "\u00dcres" + }, "step": { "options_binary": { "data": { - "name": "N\u00e9v (nem k\u00f6telez\u0151)" - } + "inverse": "Invert\u00e1lja a nyitott/z\u00e1rt \u00e1llapotot", + "name": "N\u00e9v (nem k\u00f6telez\u0151)", + "type": "Bin\u00e1ris \u00e9rz\u00e9kel\u0151 t\u00edpusa" + }, + "description": "{zone} opci\u00f3k", + "title": "Bin\u00e1ris \u00e9rz\u00e9kel\u0151 konfigur\u00e1l\u00e1sa" }, "options_digital": { "data": { "name": "N\u00e9v (nem k\u00f6telez\u0151)", "poll_interval": "Lek\u00e9rdez\u00e9si id\u0151k\u00f6z (perc) (opcion\u00e1lis)", "type": "\u00c9rz\u00e9kel\u0151 t\u00edpusa" - } + }, + "description": "{zone} opci\u00f3k", + "title": "Digit\u00e1lis \u00e9rz\u00e9kel\u0151 konfigur\u00e1l\u00e1sa" + }, + "options_io": { + "data": { + "1": "1. z\u00f3na", + "2": "2. z\u00f3na", + "3": "3. z\u00f3na", + "4": "4. z\u00f3na", + "5": "5. z\u00f3na", + "6": "6. z\u00f3na", + "7": "7. z\u00f3na", + "out": "KI" + }, + "description": "{model} felfedez\u00e9se {host}-n\u00e1l. V\u00e1lassza ki az al\u00e1bbi I/O alapkonfigur\u00e1ci\u00f3j\u00e1t - az I/O-t\u00f3l f\u00fcgg\u0151en lehet\u0151v\u00e9 teheti bin\u00e1ris \u00e9rz\u00e9kel\u0151k (nyitott/k\u00f6zeli \u00e9rintkez\u0151k), digit\u00e1lis \u00e9rz\u00e9kel\u0151k (dht \u00e9s ds18b20) vagy kapcsolhat\u00f3 kimenetek sz\u00e1m\u00e1ra. A r\u00e9szletes be\u00e1ll\u00edt\u00e1sokat a k\u00f6vetkez\u0151 l\u00e9p\u00e9sekben konfigur\u00e1lhatja.", + "title": "I/O konfigur\u00e1l\u00e1sa" + }, + "options_io_ext": { + "data": { + "10": "10. z\u00f3na", + "11": "11. z\u00f3na", + "12": "12. z\u00f3na", + "8": "8. z\u00f3na", + "9": "9. z\u00f3na", + "alarm1": "RIASZT\u00c1S1", + "alarm2_out2": "KI2/RIASZT\u00c1S2", + "out1": "KI1" + }, + "description": "V\u00e1lassza ki a fennmarad\u00f3 I/O konfigur\u00e1ci\u00f3j\u00e1t al\u00e1bb. A k\u00f6vetkez\u0151 l\u00e9p\u00e9sekben konfigur\u00e1lhatja a r\u00e9szletes be\u00e1ll\u00edt\u00e1sokat.", + "title": "B\u0151v\u00edtett I/O konfigur\u00e1l\u00e1sa" + }, + "options_misc": { + "data": { + "api_host": "API host URL fel\u00fclb\u00edr\u00e1l\u00e1sa (opcion\u00e1lis)", + "blink": "A panel LED villog\u00e1sa \u00e1llapotv\u00e1ltoz\u00e1skor", + "discovery": "V\u00e1laszoljon a h\u00e1l\u00f3zaton \u00e9rkez\u0151 felder\u00edt\u00e9si k\u00e9r\u00e9sekre", + "override_api_host": "Az alap\u00e9rtelmezett Home Assistant API gazdag\u00e9p-URL fel\u00fcl\u00edr\u00e1sa" + }, + "description": "K\u00e9rj\u00fck, v\u00e1lassza ki a k\u00edv\u00e1nt viselked\u00e9st a panelhez", + "title": "Egy\u00e9b be\u00e1ll\u00edt\u00e1sa" }, "options_switch": { "data": { - "name": "N\u00e9v (nem k\u00f6telez\u0151)" - } + "activation": "Kimenet bekapcsolt \u00e1llapotban", + "momentary": "Impulzus id\u0151tartama (ms) (opcion\u00e1lis)", + "more_states": "Tov\u00e1bbi \u00e1llapotok konfigur\u00e1l\u00e1sa ehhez a z\u00f3n\u00e1hoz", + "name": "N\u00e9v (nem k\u00f6telez\u0151)", + "pause": "Sz\u00fcnet impulzusok k\u00f6z\u00f6tt (ms) (opcion\u00e1lis)", + "repeat": "Ism\u00e9tl\u00e9si id\u0151k (-1 = v\u00e9gtelen) (opcion\u00e1lis)" + }, + "description": "{zone} opci\u00f3k: \u00e1llapot {state}", + "title": "Kapcsolhat\u00f3 kimenet konfigur\u00e1l\u00e1sa" } } } diff --git a/homeassistant/components/kraken/translations/nl.json b/homeassistant/components/kraken/translations/nl.json index 7de89d6b2dc..25fe63bebd5 100644 --- a/homeassistant/components/kraken/translations/nl.json +++ b/homeassistant/components/kraken/translations/nl.json @@ -9,10 +9,6 @@ }, "step": { "user": { - "data": { - "one": "Leeg", - "other": "Leeg" - }, "description": "Wil je beginnen met instellen?" } } diff --git a/homeassistant/components/life360/translations/hu.json b/homeassistant/components/life360/translations/hu.json index 603efee6d9d..5dbd2898971 100644 --- a/homeassistant/components/life360/translations/hu.json +++ b/homeassistant/components/life360/translations/hu.json @@ -18,7 +18,9 @@ "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "A speci\u00e1lis be\u00e1ll\u00edt\u00e1sok megad\u00e1s\u00e1hoz l\u00e1sd a [Life360 dokument\u00e1ci\u00f3]({docs_url}) c\u00edm\u0171 r\u00e9szt.\n \u00c9rdemes ezt megtenni a fi\u00f3kok hozz\u00e1ad\u00e1sa el\u0151tt.", + "title": "Life360 fi\u00f3kadatok" } } } diff --git a/homeassistant/components/litejet/translations/hu.json b/homeassistant/components/litejet/translations/hu.json index 3ee53c086bf..910d34cdc1a 100644 --- a/homeassistant/components/litejet/translations/hu.json +++ b/homeassistant/components/litejet/translations/hu.json @@ -15,5 +15,15 @@ "title": "Csatlakoz\u00e1s a LiteJet-hez" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "Alap\u00e9rtelmezett \u00e1tmenet (m\u00e1sodperc)" + }, + "title": "A LiteJet konfigur\u00e1l\u00e1sa" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/logi_circle/translations/hu.json b/homeassistant/components/logi_circle/translations/hu.json index 9c788350de4..73522a59519 100644 --- a/homeassistant/components/logi_circle/translations/hu.json +++ b/homeassistant/components/logi_circle/translations/hu.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "external_error": "Kiv\u00e9tel t\u00f6rt\u00e9nt egy m\u00e1sik folyamatb\u00f3l.", + "external_setup": "LogiCircle sikeresen konfigur\u00e1lva egy m\u00e1sik folyamatb\u00f3l.", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t." }, "error": { @@ -10,10 +12,15 @@ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { + "auth": { + "description": "K\u00e9rj\u00fck, k\u00f6vesse az al\u00e1bbi linket, \u00e9s ** Fogadja el ** a LogiCircle -fi\u00f3kj\u00e1hoz val\u00f3 hozz\u00e1f\u00e9r\u00e9st, majd t\u00e9rjen vissza, \u00e9s nyomja meg az al\u00e1bbi ** K\u00fcld\u00e9s ** gombot. \n\n [Link] ({authorization_url})", + "title": "Hiteles\u00edt\u00e9s a LogiCircle seg\u00edts\u00e9g\u00e9vel" + }, "user": { "data": { "flow_impl": "Szolg\u00e1ltat\u00f3" }, + "description": "V\u00e1lassza ki, melyik hiteles\u00edt\u00e9si szolg\u00e1ltat\u00f3n kereszt\u00fcl szeretn\u00e9 hiteles\u00edteni a LogiCircle szolg\u00e1ltat\u00e1st.", "title": "Hiteles\u00edt\u00e9si Szolg\u00e1ltat\u00f3" } } diff --git a/homeassistant/components/lutron_caseta/translations/hu.json b/homeassistant/components/lutron_caseta/translations/hu.json index 905fc05bf8e..0e8960530e3 100644 --- a/homeassistant/components/lutron_caseta/translations/hu.json +++ b/homeassistant/components/lutron_caseta/translations/hu.json @@ -10,6 +10,10 @@ }, "flow_title": "{name} ({host})", "step": { + "import_failed": { + "description": "Nem siker\u00fclt be\u00e1ll\u00edtani a bridge-t ({host}) a configuration.yaml f\u00e1jlb\u00f3l import\u00e1lva.", + "title": "Nem siker\u00fclt import\u00e1lni a Cas\u00e9ta h\u00edd konfigur\u00e1ci\u00f3j\u00e1t." + }, "link": { "description": "A(z) {name} {host} p\u00e1ros\u00edt\u00e1s\u00e1hoz az \u0171rlap elk\u00fcld\u00e9se ut\u00e1n nyomja meg a h\u00edd h\u00e1tulj\u00e1n tal\u00e1lhat\u00f3 fekete gombot.", "title": "P\u00e1ros\u00edtsd a h\u00edddal" diff --git a/homeassistant/components/melcloud/translations/hu.json b/homeassistant/components/melcloud/translations/hu.json index 7f81269c700..5744b71c780 100644 --- a/homeassistant/components/melcloud/translations/hu.json +++ b/homeassistant/components/melcloud/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "A MELCloud integr\u00e1ci\u00f3 m\u00e1r be van \u00e1ll\u00edtva ehhez az e-mailhez. A hozz\u00e1f\u00e9r\u00e9si token friss\u00edtve lett." + }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", @@ -10,7 +13,9 @@ "data": { "password": "Jelsz\u00f3", "username": "E-mail" - } + }, + "description": "Csatlakozzon a MELCloud-fi\u00f3kj\u00e1val.", + "title": "Csatlakozzon a MELCloudhoz" } } } diff --git a/homeassistant/components/minecraft_server/translations/hu.json b/homeassistant/components/minecraft_server/translations/hu.json index 247c1ffc1c3..ef3c228d2d5 100644 --- a/homeassistant/components/minecraft_server/translations/hu.json +++ b/homeassistant/components/minecraft_server/translations/hu.json @@ -4,7 +4,9 @@ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Nem siker\u00fclt csatlakozni a szerverhez. K\u00e9rj\u00fck, ellen\u0151rizze a gazdag\u00e9pet \u00e9s a portot, majd pr\u00f3b\u00e1lkozzon \u00fajra. Gondoskodjon arr\u00f3l, hogy a szerveren legal\u00e1bb a Minecraft 1.7-es verzi\u00f3j\u00e1t futtassa." + "cannot_connect": "Nem siker\u00fclt csatlakozni a szerverhez. K\u00e9rj\u00fck, ellen\u0151rizze a gazdag\u00e9pet \u00e9s a portot, majd pr\u00f3b\u00e1lkozzon \u00fajra. Gondoskodjon arr\u00f3l, hogy a szerveren legal\u00e1bb a Minecraft 1.7-es verzi\u00f3j\u00e1t futtassa.", + "invalid_ip": "Az IP -c\u00edm \u00e9rv\u00e9nytelen (a MAC -c\u00edmet nem siker\u00fclt meghat\u00e1rozni). K\u00e9rj\u00fck, jav\u00edtsa ki, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", + "invalid_port": "A portnak 1024 \u00e9s 65535 k\u00f6z\u00f6tt kell lennie. K\u00e9rj\u00fck, jav\u00edtsa ki, \u00e9s pr\u00f3b\u00e1lja \u00fajra." }, "step": { "user": { @@ -12,6 +14,7 @@ "host": "Hoszt", "name": "N\u00e9v" }, + "description": "\u00c1ll\u00edtsa be a Minecraft Server p\u00e9ld\u00e1nyt, hogy lehet\u0151v\u00e9 tegye a megfigyel\u00e9st.", "title": "Kapcsold \u00f6ssze a Minecraft szervered" } } diff --git a/homeassistant/components/monoprice/translations/hu.json b/homeassistant/components/monoprice/translations/hu.json index a845f862160..fd11a8fbc0f 100644 --- a/homeassistant/components/monoprice/translations/hu.json +++ b/homeassistant/components/monoprice/translations/hu.json @@ -10,8 +10,30 @@ "step": { "user": { "data": { - "port": "Port" - } + "port": "Port", + "source_1": "Forr\u00e1s neve #1", + "source_2": "Forr\u00e1s neve #2", + "source_3": "Forr\u00e1s neve #3", + "source_4": "Forr\u00e1s neve #4", + "source_5": "Forr\u00e1s neve #5", + "source_6": "Forr\u00e1s neve #6" + }, + "title": "Csatlakoz\u00e1s az eszk\u00f6zh\u00f6z" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Forr\u00e1s neve #1", + "source_2": "Forr\u00e1s neve #2", + "source_3": "Forr\u00e1s neve #3", + "source_4": "Forr\u00e1s neve #4", + "source_5": "Forr\u00e1s neve #5", + "source_6": "Forr\u00e1s neve #6" + }, + "title": "Forr\u00e1sok konfigur\u00e1l\u00e1sa" } } } diff --git a/homeassistant/components/mqtt/translations/hu.json b/homeassistant/components/mqtt/translations/hu.json index 84c4a40f082..a519cab55d3 100644 --- a/homeassistant/components/mqtt/translations/hu.json +++ b/homeassistant/components/mqtt/translations/hu.json @@ -50,6 +50,8 @@ }, "options": { "error": { + "bad_birth": "\u00c9rv\u00e9nytelen sz\u00fclet\u00e9si t\u00e9ma.", + "bad_will": "\u00c9rv\u00e9nytelen t\u00e9ma.", "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { @@ -59,9 +61,25 @@ "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "K\u00e9rlek, add meg az MQTT br\u00f3ker kapcsol\u00f3d\u00e1si adatait.", + "title": "Br\u00f3ker opci\u00f3k" }, "options": { + "data": { + "birth_enable": "Sz\u00fclet\u00e9si \u00fczenet enged\u00e9lyez\u00e9se", + "birth_payload": "Sz\u00fclet\u00e9si \u00fczenet", + "birth_qos": "Sz\u00fclet\u00e9si \u00fczenet QoS", + "birth_retain": "A sz\u00fclet\u00e9si \u00fczenet meg\u0151rz\u00e9se", + "birth_topic": "Sz\u00fclet\u00e9si \u00fczenet t\u00e9m\u00e1ja", + "discovery": "Felfedez\u00e9s enged\u00e9lyez\u00e9se", + "will_enable": "Enged\u00e9lyez\u00e9si \u00fczenet", + "will_payload": "\u00dczenet", + "will_qos": "QoS \u00fczenet", + "will_retain": "\u00dczenet megtart\u00e1sa", + "will_topic": "\u00dczenet t\u00e9m\u00e1ja" + }, + "description": "Felfedez\u00e9s - Ha a felfedez\u00e9s enged\u00e9lyezve van (aj\u00e1nlott), a Home Assistant automatikusan felfedezi azokat az eszk\u00f6z\u00f6ket \u00e9s entit\u00e1sokat, amelyek k\u00f6zz\u00e9teszik konfigur\u00e1ci\u00f3jukat az MQTT br\u00f3keren. Ha a felfedez\u00e9s le van tiltva, minden konfigur\u00e1ci\u00f3t manu\u00e1lisan kell elv\u00e9gezni.\nSz\u00fclet\u00e9si \u00fczenet - A sz\u00fclet\u00e9si \u00fczenetet minden alkalommal elk\u00fcldi, amikor a Home Assistant (\u00fajra) csatlakozik az MQTT br\u00f3kerhez.\nAkarat \u00fczenet - Az akarat\u00fczenet minden alkalommal el lesz k\u00fcldve, amikor a Home Assistant elvesz\u00edti a kapcsolatot a k\u00f6zvet\u00edt\u0151vel, mind takar\u00edt\u00e1s eset\u00e9n (pl. A Home Assistant le\u00e1ll\u00edt\u00e1sa), mind tiszt\u00e1talans\u00e1g eset\u00e9n (pl. Home Assistant \u00f6sszeomlik vagy megszakad a h\u00e1l\u00f3zati kapcsolata) bontani.", "title": "MQTT opci\u00f3k" } } diff --git a/homeassistant/components/myq/translations/hu.json b/homeassistant/components/myq/translations/hu.json index 59338cf43ae..f50099f023b 100644 --- a/homeassistant/components/myq/translations/hu.json +++ b/homeassistant/components/myq/translations/hu.json @@ -21,7 +21,8 @@ "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "Csatlakozzon a MyQ Gateway-hez" } } } diff --git a/homeassistant/components/netatmo/translations/hu.json b/homeassistant/components/netatmo/translations/hu.json index 0e6536bb0ad..48f084f84c2 100644 --- a/homeassistant/components/netatmo/translations/hu.json +++ b/homeassistant/components/netatmo/translations/hu.json @@ -41,14 +41,24 @@ "step": { "public_weather": { "data": { - "area_name": "A ter\u00fclet neve" - } + "area_name": "A ter\u00fclet neve", + "lat_ne": "Sz\u00e9less\u00e9g \u00c9szakkeleti sarok", + "lat_sw": "Sz\u00e9less\u00e9g D\u00e9lnyugati sarok", + "lon_ne": "Hossz\u00fas\u00e1g \u00c9szakkeleti sarok", + "lon_sw": "Hossz\u00fas\u00e1g D\u00e9lnyugati sarok", + "mode": "Sz\u00e1m\u00edt\u00e1s", + "show_on_map": "Mutasd a t\u00e9rk\u00e9pen" + }, + "description": "\u00c1ll\u00edtson be egy nyilv\u00e1nos id\u0151j\u00e1r\u00e1s-\u00e9rz\u00e9kel\u0151t egy ter\u00fclethez.", + "title": "Netatmo nyilv\u00e1nos id\u0151j\u00e1r\u00e1s-\u00e9rz\u00e9kel\u0151" }, "public_weather_areas": { "data": { "new_area": "Ter\u00fclet neve", "weather_areas": "Id\u0151j\u00e1r\u00e1si ter\u00fcletek" - } + }, + "description": "\u00c1ll\u00edtsa be a nyilv\u00e1nos id\u0151j\u00e1r\u00e1s-\u00e9rz\u00e9kel\u0151ket.", + "title": "Netatmo nyilv\u00e1nos id\u0151j\u00e1r\u00e1s-\u00e9rz\u00e9kel\u0151" } } } diff --git a/homeassistant/components/nfandroidtv/translations/hu.json b/homeassistant/components/nfandroidtv/translations/hu.json new file mode 100644 index 00000000000..e7dea95e4d0 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "user": { + "data": { + "host": "H\u00e1zigazda", + "name": "N\u00e9v" + }, + "description": "Ehhez az integr\u00e1ci\u00f3hoz az \u00c9rtes\u00edt\u00e9sek az Android TV alkalmaz\u00e1shoz sz\u00fcks\u00e9ges. \n\nAndroid TV eset\u00e9n: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nA Fire TV eset\u00e9ben: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\nBe kell \u00e1ll\u00edtania a DHCP -foglal\u00e1st az \u00fatv\u00e1laszt\u00f3n (l\u00e1sd az \u00fatv\u00e1laszt\u00f3 felhaszn\u00e1l\u00f3i k\u00e9zik\u00f6nyv\u00e9t), vagy egy statikus IP -c\u00edmet az eszk\u00f6z\u00f6n. Ha nem, az eszk\u00f6z v\u00e9g\u00fcl el\u00e9rhetetlenn\u00e9 v\u00e1lik.", + "title": "\u00c9rtes\u00edt\u00e9sek Android TV / Fire TV eset\u00e9n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/hu.json b/homeassistant/components/nuheat/translations/hu.json index e6e7174e325..873b03cebff 100644 --- a/homeassistant/components/nuheat/translations/hu.json +++ b/homeassistant/components/nuheat/translations/hu.json @@ -15,7 +15,9 @@ "password": "Jelsz\u00f3", "serial_number": "A termoszt\u00e1t sorozatsz\u00e1ma.", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "A termoszt\u00e1t numerikus sorozatsz\u00e1m\u00e1t vagy azonos\u00edt\u00f3j\u00e1t meg kell szereznie, ha bejelentkezik a https://MyNuHeat.com oldalra, \u00e9s kiv\u00e1lasztja a termoszt\u00e1tot.", + "title": "Csatlakozzon a NuHeat-hez" } } } diff --git a/homeassistant/components/nut/translations/hu.json b/homeassistant/components/nut/translations/hu.json index a7bad455dc3..bfc8e01c11a 100644 --- a/homeassistant/components/nut/translations/hu.json +++ b/homeassistant/components/nut/translations/hu.json @@ -8,13 +8,27 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "resources": { + "data": { + "resources": "Forr\u00e1sok" + }, + "title": "V\u00e1lassza ki a nyomon k\u00f6vetend\u0151 er\u0151forr\u00e1sokat" + }, + "ups": { + "data": { + "alias": "\u00c1ln\u00e9v", + "resources": "Forr\u00e1sok" + }, + "title": "V\u00e1lassza ki a fel\u00fcgyelni k\u00edv\u00e1nt UPS-t" + }, "user": { "data": { "host": "Hoszt", "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "Csatlakozzon a NUT szerverhez" } } }, @@ -22,6 +36,15 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "init": { + "data": { + "resources": "Forr\u00e1sok", + "scan_interval": "Szkennel\u00e9si intervallum (m\u00e1sodperc)" + }, + "description": "V\u00e1lassza az \u00c9rz\u00e9kel\u0151 er\u0151forr\u00e1sokat." + } } } } \ No newline at end of file diff --git a/homeassistant/components/nws/translations/hu.json b/homeassistant/components/nws/translations/hu.json index 1d674cacc7e..ec9bf3f4988 100644 --- a/homeassistant/components/nws/translations/hu.json +++ b/homeassistant/components/nws/translations/hu.json @@ -12,8 +12,11 @@ "data": { "api_key": "API kulcs", "latitude": "Sz\u00e9less\u00e9g", - "longitude": "Hossz\u00fas\u00e1g" - } + "longitude": "Hossz\u00fas\u00e1g", + "station": "METAR \u00e1llom\u00e1s k\u00f3dja" + }, + "description": "Ha a METAR \u00e1llom\u00e1s k\u00f3dja nincs megadva, a sz\u00e9less\u00e9gi \u00e9s hossz\u00fas\u00e1gi fokokat haszn\u00e1lja a legk\u00f6zelebbi \u00e1llom\u00e1s megkeres\u00e9s\u00e9hez. Egyel\u0151re az API-kulcs b\u00e1rmi lehet. Javasoljuk, hogy \u00e9rv\u00e9nyes e -mail c\u00edmet haszn\u00e1ljon.", + "title": "Csatlakozzon az National Weather Service-hez" } } } diff --git a/homeassistant/components/onvif/translations/hu.json b/homeassistant/components/onvif/translations/hu.json index e2b63a6c9d8..c43df53ae9f 100644 --- a/homeassistant/components/onvif/translations/hu.json +++ b/homeassistant/components/onvif/translations/hu.json @@ -53,7 +53,19 @@ "data": { "auto": "Automatikus keres\u00e9s" }, - "description": "A k\u00fcld\u00e9s gombra kattintva olyan ONVIF-eszk\u00f6z\u00f6ket keres\u00fcnk a h\u00e1l\u00f3zat\u00e1ban, amelyek t\u00e1mogatj\u00e1k az S profilt.\n\nEgyes gy\u00e1rt\u00f3k alap\u00e9rtelmez\u00e9s szerint elkezdt\u00e9k letiltani az ONVIF-et. Ellen\u0151rizze, hogy az ONVIF enged\u00e9lyezve van-e a kamera konfigur\u00e1ci\u00f3j\u00e1ban." + "description": "A k\u00fcld\u00e9s gombra kattintva olyan ONVIF-eszk\u00f6z\u00f6ket keres\u00fcnk a h\u00e1l\u00f3zat\u00e1ban, amelyek t\u00e1mogatj\u00e1k az S profilt.\n\nEgyes gy\u00e1rt\u00f3k alap\u00e9rtelmez\u00e9s szerint elkezdt\u00e9k letiltani az ONVIF-et. Ellen\u0151rizze, hogy az ONVIF enged\u00e9lyezve van-e a kamera konfigur\u00e1ci\u00f3j\u00e1ban.", + "title": "ONVIF eszk\u00f6z be\u00e1ll\u00edt\u00e1sa" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Extra FFMPEG opci\u00f3k", + "rtsp_transport": "RTSP sz\u00e1ll\u00edt\u00e1si mechanizmus" + }, + "title": "ONVIF eszk\u00f6z opci\u00f3i" } } } diff --git a/homeassistant/components/opentherm_gw/translations/hu.json b/homeassistant/components/opentherm_gw/translations/hu.json index 77112bd8929..3127dc523ce 100644 --- a/homeassistant/components/opentherm_gw/translations/hu.json +++ b/homeassistant/components/opentherm_gw/translations/hu.json @@ -24,7 +24,8 @@ "read_precision": "Pontoss\u00e1g olvas\u00e1sa", "set_precision": "Pontoss\u00e1g be\u00e1ll\u00edt\u00e1sa", "temporary_override_mode": "Ideiglenes be\u00e1ll\u00edt\u00e1s fel\u00fclb\u00edr\u00e1l\u00e1si m\u00f3dja" - } + }, + "description": "Opci\u00f3k az OpenTherm \u00e1tj\u00e1r\u00f3hoz" } } } diff --git a/homeassistant/components/ovo_energy/translations/hu.json b/homeassistant/components/ovo_energy/translations/hu.json index 143d1a8dc18..2c794b5cd9d 100644 --- a/homeassistant/components/ovo_energy/translations/hu.json +++ b/homeassistant/components/ovo_energy/translations/hu.json @@ -19,6 +19,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, + "description": "\u00c1ll\u00edtson be egy OVO Energy p\u00e9ld\u00e1nyt az energiafelhaszn\u00e1l\u00e1s el\u00e9r\u00e9s\u00e9hez.", "title": "OVO Energy azonos\u00edt\u00f3 megad\u00e1sa" } } diff --git a/homeassistant/components/ozw/translations/hu.json b/homeassistant/components/ozw/translations/hu.json index 70934bf3472..a43f234c909 100644 --- a/homeassistant/components/ozw/translations/hu.json +++ b/homeassistant/components/ozw/translations/hu.json @@ -6,6 +6,7 @@ "addon_set_config_failed": "Nem siker\u00fclt be\u00e1ll\u00edtani az OpenZWave konfigur\u00e1ci\u00f3t.", "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "mqtt_required": "Az MQTT integr\u00e1ci\u00f3 nincs be\u00e1ll\u00edtva", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "error": { diff --git a/homeassistant/components/panasonic_viera/translations/hu.json b/homeassistant/components/panasonic_viera/translations/hu.json index cfc0be387d0..df520bb1ca5 100644 --- a/homeassistant/components/panasonic_viera/translations/hu.json +++ b/homeassistant/components/panasonic_viera/translations/hu.json @@ -22,7 +22,8 @@ "host": "IP c\u00edm", "name": "N\u00e9v" }, - "description": "Add meg a Panasonic Viera TV-hez tartoz\u00f3 IP c\u00edmet" + "description": "Add meg a Panasonic Viera TV-hez tartoz\u00f3 IP c\u00edmet", + "title": "A TV be\u00e1ll\u00edt\u00e1sa" } } } diff --git a/homeassistant/components/plex/translations/hu.json b/homeassistant/components/plex/translations/hu.json index 9168f070609..c0ecbe3e02c 100644 --- a/homeassistant/components/plex/translations/hu.json +++ b/homeassistant/components/plex/translations/hu.json @@ -10,8 +10,10 @@ }, "error": { "faulty_credentials": "A hiteles\u00edt\u00e9s sikertelen", + "host_or_token": "Legal\u00e1bb egyet kell megadnia a Gazdag\u00e9p vagy a Token k\u00f6z\u00fcl", "no_servers": "Nincs szerver csatlakoztatva a fi\u00f3khoz", - "not_found": "A Plex szerver nem tal\u00e1lhat\u00f3" + "not_found": "A Plex szerver nem tal\u00e1lhat\u00f3", + "ssl_error": "SSL tan\u00fas\u00edtv\u00e1ny probl\u00e9ma" }, "flow_title": "{name} ({host})", "step": { @@ -22,7 +24,8 @@ "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", "token": "Token (opcion\u00e1lis)", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" - } + }, + "title": "K\u00e9zi Plex konfigur\u00e1ci\u00f3" }, "select_server": { "data": { @@ -32,9 +35,13 @@ "title": "Plex-kiszolg\u00e1l\u00f3 kiv\u00e1laszt\u00e1sa" }, "user": { + "description": "Folytassa a [plex.tv] (https://plex.tv) oldalt a Plex szerver \u00f6sszekapcsol\u00e1s\u00e1hoz.", "title": "Plex Media Server" }, "user_advanced": { + "data": { + "setup_method": "Be\u00e1ll\u00edt\u00e1si m\u00f3dszer" + }, "title": "Plex Media Server" } } @@ -43,7 +50,9 @@ "step": { "plex_mp_settings": { "data": { + "ignore_new_shared_users": "Hagyja figyelmen k\u00edv\u00fcl az \u00faj kezelt/megosztott felhaszn\u00e1l\u00f3kat", "ignore_plex_web_clients": "Plex Web kliensek figyelmen k\u00edv\u00fcl hagy\u00e1sa", + "monitored_users": "Megfigyelt felhaszn\u00e1l\u00f3k", "use_episode_art": "Haszn\u00e1lja az epiz\u00f3d bor\u00edt\u00f3j\u00e1t" }, "description": "Plex media lej\u00e1tsz\u00f3k be\u00e1ll\u00edt\u00e1sai" diff --git a/homeassistant/components/powerwall/translations/hu.json b/homeassistant/components/powerwall/translations/hu.json index 9f12342595a..1102ba78673 100644 --- a/homeassistant/components/powerwall/translations/hu.json +++ b/homeassistant/components/powerwall/translations/hu.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "wrong_version": "Az powerwall nem t\u00e1mogatott szoftververzi\u00f3t haszn\u00e1l. K\u00e9rj\u00fck, fontolja meg a probl\u00e9ma friss\u00edt\u00e9s\u00e9t vagy jelent\u00e9s\u00e9t, hogy megoldhat\u00f3 legyen." }, "flow_title": "{ip_address}", "step": { @@ -15,7 +16,8 @@ "data": { "ip_address": "IP c\u00edm", "password": "Jelsz\u00f3" - } + }, + "title": "Csatlakoz\u00e1s a powerwallhoz" } } } diff --git a/homeassistant/components/prosegur/translations/hu.json b/homeassistant/components/prosegur/translations/hu.json new file mode 100644 index 00000000000..143ae78d534 --- /dev/null +++ b/homeassistant/components/prosegur/translations/hu.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az ism\u00e9telt hiteles\u00edt\u00e9s sikeres volt" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "Hiteles\u00edtse \u00fajra Prosegur-fi\u00f3kkal.", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + }, + "user": { + "data": { + "country": "Orsz\u00e1g", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/hu.json b/homeassistant/components/pvpc_hourly_pricing/translations/hu.json index 0b980bd58e0..1f706862ee1 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/hu.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/hu.json @@ -6,10 +6,13 @@ "step": { "user": { "data": { + "name": "\u00c9rz\u00e9kel\u0151 neve", "power": "Szerz\u0151d\u00e9s szerinti teljes\u00edtm\u00e9ny (kW)", - "power_p3": "Szerz\u0151d\u00f6tt teljes\u00edtm\u00e9ny P3 v\u00f6lgyid\u0151szakra (kW)" + "power_p3": "Szerz\u0151d\u00f6tt teljes\u00edtm\u00e9ny P3 v\u00f6lgyid\u0151szakra (kW)", + "tariff": "Alkalmazand\u00f3 tarifa f\u00f6ldrajzi z\u00f3n\u00e1nk\u00e9nt" }, - "description": "Ez az \u00e9rz\u00e9kel\u0151 a hivatalos API-t haszn\u00e1lja a [villamos energia \u00f3r\u00e1nk\u00e9nti \u00e1raz\u00e1s\u00e1nak (PVPC)] (https://www.esios.ree.es/es/pvpc) megszerz\u00e9s\u00e9hez Spanyolorsz\u00e1gban.\n Pontosabb magyar\u00e1zat\u00e9rt keresse fel az [integr\u00e1ci\u00f3s dokumentumok] oldalt (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/)." + "description": "Ez az \u00e9rz\u00e9kel\u0151 a hivatalos API-t haszn\u00e1lja a [villamos energia \u00f3r\u00e1nk\u00e9nti \u00e1raz\u00e1s\u00e1nak (PVPC)] (https://www.esios.ree.es/es/pvpc) megszerz\u00e9s\u00e9hez Spanyolorsz\u00e1gban.\n Pontosabb magyar\u00e1zat\u00e9rt keresse fel az [integr\u00e1ci\u00f3s dokumentumok] oldalt (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "\u00c9rz\u00e9kel\u0151 be\u00e1ll\u00edt\u00e1sa" } } }, diff --git a/homeassistant/components/rachio/translations/hu.json b/homeassistant/components/rachio/translations/hu.json index 570dd27b5d9..0c6112988d8 100644 --- a/homeassistant/components/rachio/translations/hu.json +++ b/homeassistant/components/rachio/translations/hu.json @@ -12,6 +12,17 @@ "user": { "data": { "api_key": "API kulcs" + }, + "description": "Sz\u00fcks\u00e9ge lesz az API-kulcsra a https://app.rach.io/ webhelyen. L\u00e9pjen a Be\u00e1ll\u00edt\u00e1sok elemre, majd kattintson az \u201eAPI KEY GET\u201d lek\u00e9r\u00e9s\u00e9re.", + "title": "Csatlakozzon a Rachio k\u00e9sz\u00fcl\u00e9khez" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "A fut\u00e1s id\u0151tartama percben a z\u00f3nakapcsol\u00f3 aktiv\u00e1l\u00e1sakor" } } } diff --git a/homeassistant/components/renault/translations/he.json b/homeassistant/components/renault/translations/he.json index d20e2d36a81..25cec1032e9 100644 --- a/homeassistant/components/renault/translations/he.json +++ b/homeassistant/components/renault/translations/he.json @@ -11,7 +11,8 @@ "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05d3\u05d5\u05d0\"\u05dc" - } + }, + "title": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8\u05d9 \u05e8\u05e0\u05d5" } } } diff --git a/homeassistant/components/renault/translations/hu.json b/homeassistant/components/renault/translations/hu.json new file mode 100644 index 00000000000..eeace0b9b85 --- /dev/null +++ b/homeassistant/components/renault/translations/hu.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "kamereon_no_account": "Nem tal\u00e1lhat\u00f3 a Kamereon-fi\u00f3k." + }, + "error": { + "invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereon-fi\u00f3k azonos\u00edt\u00f3ja" + }, + "title": "V\u00e1lassza ki a Kamereon-fi\u00f3k azonos\u00edt\u00f3j\u00e1t" + }, + "user": { + "data": { + "locale": "Helysz\u00edn", + "password": "Jelsz\u00f3", + "username": "Email" + }, + "title": "\u00c1ll\u00edtsa be a Renault hiteles\u00edt\u0151 adatait" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/hu.json b/homeassistant/components/roku/translations/hu.json index 5485d9e00ce..b7aa12bfb4d 100644 --- a/homeassistant/components/roku/translations/hu.json +++ b/homeassistant/components/roku/translations/hu.json @@ -19,13 +19,18 @@ "title": "Roku" }, "ssdp_confirm": { + "data": { + "one": "\u00dcres", + "other": "\u00dcres" + }, "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}-t?", "title": "Roku" }, "user": { "data": { "host": "Hoszt" - } + }, + "description": "Adja meg Roku adatait." } } } diff --git a/homeassistant/components/roomba/translations/hu.json b/homeassistant/components/roomba/translations/hu.json index 2f8d902f4fe..0d76ce920b2 100644 --- a/homeassistant/components/roomba/translations/hu.json +++ b/homeassistant/components/roomba/translations/hu.json @@ -40,10 +40,12 @@ "user": { "data": { "blid": "BLID", + "continuous": "Folyamatos", "delay": "K\u00e9sleltet\u00e9s", "host": "Hoszt", "password": "Jelsz\u00f3" }, + "description": "V\u00e1lasszon Roomba-t vagy Braava-t.", "title": "Csatlakoz\u00e1s az eszk\u00f6zh\u00f6z" } } diff --git a/homeassistant/components/roon/translations/hu.json b/homeassistant/components/roon/translations/hu.json index 123027a8216..56a8ade165c 100644 --- a/homeassistant/components/roon/translations/hu.json +++ b/homeassistant/components/roon/translations/hu.json @@ -9,12 +9,14 @@ }, "step": { "link": { + "description": "Enged\u00e9lyeznie kell az HomeAssistantot a Roonban. Miut\u00e1n r\u00e1kattintott a K\u00fcld\u00e9s gombra, nyissa meg a Roon Core alkalmaz\u00e1st, nyissa meg a Be\u00e1ll\u00edt\u00e1sokat, \u00e9s enged\u00e9lyezze a HomeAssistant funkci\u00f3t a B\u0151v\u00edtm\u00e9nyek lapon.", "title": "Enged\u00e9lyezze a HomeAssistant alkalmaz\u00e1st Roon-ban" }, "user": { "data": { "host": "Hoszt" - } + }, + "description": "Nem tal\u00e1lta a Roon szervert, adja meg a gazdag\u00e9p nev\u00e9t vagy IP-c\u00edm\u00e9t." } } } diff --git a/homeassistant/components/sense/translations/hu.json b/homeassistant/components/sense/translations/hu.json index 4ecaf2ba0d0..acd67b9e6f9 100644 --- a/homeassistant/components/sense/translations/hu.json +++ b/homeassistant/components/sense/translations/hu.json @@ -13,7 +13,8 @@ "data": { "email": "E-mail", "password": "Jelsz\u00f3" - } + }, + "title": "Csatlakoztassa a Sense Energy Monitort" } } } diff --git a/homeassistant/components/sentry/translations/hu.json b/homeassistant/components/sentry/translations/hu.json index 79188df18b1..43404f72495 100644 --- a/homeassistant/components/sentry/translations/hu.json +++ b/homeassistant/components/sentry/translations/hu.json @@ -25,7 +25,8 @@ "event_custom_components": "Esem\u00e9nyek k\u00fcld\u00e9se egy\u00e9ni \u00f6sszetev\u0151kb\u0151l", "event_handled": "K\u00fcldj\u00f6n kezelt esem\u00e9nyeket", "event_third_party_packages": "K\u00fcldj\u00f6n esem\u00e9nyeket harmadik f\u00e9l csomagjaib\u00f3l", - "tracing": "Enged\u00e9lyezze a teljes\u00edtm\u00e9nyk\u00f6vet\u00e9st" + "tracing": "Enged\u00e9lyezze a teljes\u00edtm\u00e9nyk\u00f6vet\u00e9st", + "tracing_sample_rate": "A mintav\u00e9teli sebess\u00e9g nyomon k\u00f6vet\u00e9se; 0,0 \u00e9s 1,0 k\u00f6z\u00f6tt (1,0 = 100%)" } } } diff --git a/homeassistant/components/shelly/translations/hu.json b/homeassistant/components/shelly/translations/hu.json index 2c8f468aaed..9388e26515a 100644 --- a/homeassistant/components/shelly/translations/hu.json +++ b/homeassistant/components/shelly/translations/hu.json @@ -11,6 +11,9 @@ }, "flow_title": "{name}", "step": { + "confirm_discovery": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a(z) {model} a(z) {host} c\u00edmen? \n\n A jelsz\u00f3val v\u00e9dett akkumul\u00e1toros eszk\u00f6z\u00f6ket fel kell \u00e9breszteni, miel\u0151tt folytatn\u00e1 a be\u00e1ll\u00edt\u00e1st.\n Az elemmel m\u0171k\u00f6d\u0151, jelsz\u00f3val nem v\u00e9dett eszk\u00f6z\u00f6k hozz\u00e1ad\u00e1sra ker\u00fclnek, amikor az eszk\u00f6z fel\u00e9bred, most manu\u00e1lisan \u00e9bresztheti fel az eszk\u00f6zt egy rajta l\u00e9v\u0151 gombbal, vagy v\u00e1rhat a k\u00f6vetkez\u0151 adatfriss\u00edt\u00e9sre." + }, "credentials": { "data": { "password": "Jelsz\u00f3", diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index 8a2deedc534..14faee90ed4 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Ez a SimpliSafe-fi\u00f3k m\u00e1r haszn\u00e1latban van.", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" }, "error": { @@ -9,10 +10,14 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "mfa": { + "title": "SimpliSafe t\u00f6bbt\u00e9nyez\u0151s hiteles\u00edt\u00e9s" + }, "reauth_confirm": { "data": { "password": "Jelsz\u00f3" }, + "description": "Hozz\u00e1f\u00e9r\u00e9se lej\u00e1rt vagy visszavont\u00e1k. Adja meg jelszav\u00e1t a fi\u00f3k \u00fajb\u00f3li \u00f6sszekapcsol\u00e1s\u00e1hoz.", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, "user": { @@ -24,5 +29,15 @@ "title": "T\u00f6ltsd ki az adataid" } } + }, + "options": { + "step": { + "init": { + "data": { + "code": "K\u00f3d (a Home Assistant felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9n haszn\u00e1latos)" + }, + "title": "A SimpliSafe konfigur\u00e1l\u00e1sa" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/hu.json b/homeassistant/components/smappee/translations/hu.json index 5d3e65bb6fc..5b00dffde9c 100644 --- a/homeassistant/components/smappee/translations/hu.json +++ b/homeassistant/components/smappee/translations/hu.json @@ -2,8 +2,10 @@ "config": { "abort": { "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_configured_local_device": "A helyi eszk\u00f6z\u00f6k m\u00e1r konfigur\u00e1lva vannak. K\u00e9rj\u00fck, el\u0151sz\u00f6r t\u00e1vol\u00edtsa el ezeket, miel\u0151tt konfigur\u00e1lja a felh\u0151alap\u00fa eszk\u00f6zt.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_mdns": "Nem t\u00e1mogatott eszk\u00f6z a Smappee integr\u00e1ci\u00f3hoz.", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz." }, @@ -12,15 +14,21 @@ "environment": { "data": { "environment": "K\u00f6rnyezet" - } + }, + "description": "\u00c1ll\u00edtsa be a Smappee k\u00e9sz\u00fcl\u00e9ket az HomeAssistant-al val\u00f3 integr\u00e1ci\u00f3hoz." }, "local": { "data": { "host": "Hoszt" - } + }, + "description": "Adja meg a gazdag\u00e9pet a Smappee helyi integr\u00e1ci\u00f3j\u00e1nak elind\u00edt\u00e1s\u00e1hoz" }, "pick_implementation": { "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + }, + "zeroconf_confirm": { + "description": "Hozz\u00e1 szeretn\u00e9 adni a \"{serialnumber} serialnumber}\" sorozatsz\u00e1m\u00fa Smappee -eszk\u00f6zt az HomeAssistanthoz?", + "title": "Felfedezett Smappee eszk\u00f6z" } } } diff --git a/homeassistant/components/smarthab/translations/hu.json b/homeassistant/components/smarthab/translations/hu.json index 222c95bba16..2e3cf430a9f 100644 --- a/homeassistant/components/smarthab/translations/hu.json +++ b/homeassistant/components/smarthab/translations/hu.json @@ -2,6 +2,7 @@ "config": { "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "service": "Hiba t\u00f6rt\u00e9nt a SmartHab el\u00e9r\u00e9se k\u00f6zben. A szolg\u00e1ltat\u00e1s le\u00e1llhat. Ellen\u0151rizze a kapcsolatot.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { @@ -10,7 +11,8 @@ "email": "E-mail", "password": "Jelsz\u00f3" }, - "description": "Technikai okokb\u00f3l ne felejtsen el m\u00e1sodlagos fi\u00f3kot haszn\u00e1lni a Home Assistant be\u00e1ll\u00edt\u00e1s\u00e1hoz. A SmartHab alkalmaz\u00e1sb\u00f3l l\u00e9trehozhat egyet." + "description": "Technikai okokb\u00f3l ne felejtsen el m\u00e1sodlagos fi\u00f3kot haszn\u00e1lni a Home Assistant be\u00e1ll\u00edt\u00e1s\u00e1hoz. A SmartHab alkalmaz\u00e1sb\u00f3l l\u00e9trehozhat egyet.", + "title": "A SmartHab be\u00e1ll\u00edt\u00e1sa" } } } diff --git a/homeassistant/components/smartthings/translations/hu.json b/homeassistant/components/smartthings/translations/hu.json index bd6808db322..05e99bef2ea 100644 --- a/homeassistant/components/smartthings/translations/hu.json +++ b/homeassistant/components/smartthings/translations/hu.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "invalid_webhook_url": "A Home Assistant nincs megfelel\u0151en konfigur\u00e1lva a SmartThings friss\u00edt\u00e9seinek fogad\u00e1s\u00e1ra. A webhook URL \u00e9rv\u00e9nytelen:\n > {webhook_url} \n\n K\u00e9rj\u00fck, friss\u00edtse konfigur\u00e1ci\u00f3j\u00e1t az [utas\u00edt\u00e1sok] szerint ({component_url}), ind\u00edtsa \u00fajra a Home Assistant alkalmaz\u00e1st, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", + "no_available_locations": "Nincsenek be\u00e1ll\u00edthat\u00f3 SmartThings helyek a Home Assistant alkalmaz\u00e1sban." + }, "error": { "app_setup_error": "A SmartApp be\u00e1ll\u00edt\u00e1sa nem siker\u00fclt. K\u00e9rlek pr\u00f3b\u00e1ld \u00fajra.", "token_forbidden": "A token nem rendelkezik a sz\u00fcks\u00e9ges OAuth-tartom\u00e1nyokkal.", @@ -8,16 +12,22 @@ "webhook_error": "A SmartThings nem tudta \u00e9rv\u00e9nyes\u00edteni a `base_url`-ben konfigur\u00e1lt v\u00e9gpontot. K\u00e9rlek, tekintsd \u00e1t az \u00f6sszetev\u0151 k\u00f6vetelm\u00e9nyeit." }, "step": { + "authorize": { + "title": "HomeAssistant enged\u00e9lyez\u00e9se" + }, "pat": { "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token" }, - "description": "K\u00e9rj\u00fck, adjon meg egy SmartThings [Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si tokent]({token_url}), amelyet az [utas\u00edt\u00e1sok]({component_url}) alapj\u00e1n hoztak l\u00e9tre. Ezt haszn\u00e1ljuk a Home Assistant integr\u00e1ci\u00f3j\u00e1nak l\u00e9trehoz\u00e1s\u00e1hoz a SmartThings-fi\u00f3kban." + "description": "K\u00e9rj\u00fck, adjon meg egy SmartThings [Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si tokent]({token_url}), amelyet az [utas\u00edt\u00e1sok]({component_url}) alapj\u00e1n hoztak l\u00e9tre. Ezt haszn\u00e1ljuk a Home Assistant integr\u00e1ci\u00f3j\u00e1nak l\u00e9trehoz\u00e1s\u00e1hoz a SmartThings-fi\u00f3kban.", + "title": "Adja meg a szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si Tokent" }, "select_location": { "data": { "location_id": "Elhelyezked\u00e9s" - } + }, + "description": "K\u00e9rj\u00fck, v\u00e1lassza ki azt a SmartThings helyet, amelyet hozz\u00e1 szeretne adni a Home Assistant szolg\u00e1ltat\u00e1shoz. Ezut\u00e1n \u00faj ablakot nyitunk, \u00e9s megk\u00e9rj\u00fck, hogy jelentkezzen be, \u00e9s enged\u00e9lyezze a Home Assistant integr\u00e1ci\u00f3j\u00e1nak telep\u00edt\u00e9s\u00e9t a kiv\u00e1lasztott helyre.", + "title": "Hely kiv\u00e1laszt\u00e1sa" }, "user": { "description": "K\u00e9rlek add meg a SmartThings [Personal Access Tokent]({token_url}), amit az [instrukci\u00f3k]({component_url}) alapj\u00e1n hozt\u00e1l l\u00e9tre.", diff --git a/homeassistant/components/solaredge/translations/hu.json b/homeassistant/components/solaredge/translations/hu.json index 69e450f55ff..a1a14c76357 100644 --- a/homeassistant/components/solaredge/translations/hu.json +++ b/homeassistant/components/solaredge/translations/hu.json @@ -13,7 +13,8 @@ "user": { "data": { "api_key": "API kulcs", - "name": "Ennek az install\u00e1ci\u00f3nak a neve" + "name": "Ennek az install\u00e1ci\u00f3nak a neve", + "site_id": "A SolarEdge webhelyazonos\u00edt\u00f3ja" }, "title": "Az API param\u00e9terek megad\u00e1sa ehhez a telep\u00edt\u00e9shez" } diff --git a/homeassistant/components/solarlog/translations/hu.json b/homeassistant/components/solarlog/translations/hu.json index dd0ea8033ae..23baa393942 100644 --- a/homeassistant/components/solarlog/translations/hu.json +++ b/homeassistant/components/solarlog/translations/hu.json @@ -10,8 +10,10 @@ "step": { "user": { "data": { - "host": "Hoszt" - } + "host": "Hoszt", + "name": "A Solar-Log szenzorokhoz haszn\u00e1land\u00f3 el\u0151tag" + }, + "title": "Hat\u00e1rozza meg a Solar-Log kapcsolatot" } } } diff --git a/homeassistant/components/soma/translations/hu.json b/homeassistant/components/soma/translations/hu.json index d013cb49fdf..c3e572ebe0a 100644 --- a/homeassistant/components/soma/translations/hu.json +++ b/homeassistant/components/soma/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_setup": "Csak egy Soma-fi\u00f3k konfigur\u00e1lhat\u00f3.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "connection_error": "Nem siker\u00fclt csatlakozni a SOMA Connecthez.", "missing_configuration": "A Soma \u00f6sszetev\u0151 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.", diff --git a/homeassistant/components/somfy_mylink/translations/hu.json b/homeassistant/components/somfy_mylink/translations/hu.json index 5a2a1ee6ab5..093a35b78fe 100644 --- a/homeassistant/components/somfy_mylink/translations/hu.json +++ b/homeassistant/components/somfy_mylink/translations/hu.json @@ -26,6 +26,9 @@ }, "step": { "entity_config": { + "data": { + "reverse": "A bor\u00edt\u00f3 megfordult" + }, "description": "Konfigur\u00e1lja az \u201e {entity_id} \u201d be\u00e1ll\u00edt\u00e1sait", "title": "Entit\u00e1s konfigur\u00e1l\u00e1sa" }, diff --git a/homeassistant/components/speedtestdotnet/translations/hu.json b/homeassistant/components/speedtestdotnet/translations/hu.json index ec08c711e1d..cd08c3bd2d6 100644 --- a/homeassistant/components/speedtestdotnet/translations/hu.json +++ b/homeassistant/components/speedtestdotnet/translations/hu.json @@ -14,6 +14,8 @@ "step": { "init": { "data": { + "manual": "Automatikus friss\u00edt\u00e9s letilt\u00e1sa", + "scan_interval": "Friss\u00edt\u00e9si gyakoris\u00e1g (perc)", "server_name": "V\u00e1laszd ki a teszt szervert" } } diff --git a/homeassistant/components/squeezebox/translations/hu.json b/homeassistant/components/squeezebox/translations/hu.json index e9d7413ebfa..a047dbca45f 100644 --- a/homeassistant/components/squeezebox/translations/hu.json +++ b/homeassistant/components/squeezebox/translations/hu.json @@ -18,7 +18,8 @@ "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "Kapcsolati inform\u00e1ci\u00f3k szerkeszt\u00e9se" }, "user": { "data": { diff --git a/homeassistant/components/srp_energy/translations/hu.json b/homeassistant/components/srp_energy/translations/hu.json index 9ade185d831..4d617e09cfc 100644 --- a/homeassistant/components/srp_energy/translations/hu.json +++ b/homeassistant/components/srp_energy/translations/hu.json @@ -13,6 +13,7 @@ "user": { "data": { "id": "A fi\u00f3k azonos\u00edt\u00f3ja", + "is_tou": "A haszn\u00e1lati id\u0151 terv", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } diff --git a/homeassistant/components/switcher_kis/translations/hu.json b/homeassistant/components/switcher_kis/translations/hu.json new file mode 100644 index 00000000000..c3be866fb85 --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egyetlen konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "confirm": { + "description": "El akarja kezdeni a be\u00e1ll\u00edt\u00e1sokat?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthru/translations/hu.json b/homeassistant/components/syncthru/translations/hu.json index 56e7c54203d..b82b2587bc6 100644 --- a/homeassistant/components/syncthru/translations/hu.json +++ b/homeassistant/components/syncthru/translations/hu.json @@ -4,7 +4,8 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { - "invalid_url": "\u00c9rv\u00e9nytelen URL" + "invalid_url": "\u00c9rv\u00e9nytelen URL", + "unknown_state": "A nyomtat\u00f3 \u00e1llapota ismeretlen, ellen\u0151rizze az URL-t \u00e9s a h\u00e1l\u00f3zati kapcsolatot" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/synology_dsm/translations/hu.json b/homeassistant/components/synology_dsm/translations/hu.json index 7ac507f1efa..01f02e6156d 100644 --- a/homeassistant/components/synology_dsm/translations/hu.json +++ b/homeassistant/components/synology_dsm/translations/hu.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az ism\u00e9telt hiteles\u00edt\u00e9s sikeres volt" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "missing_data": "Hi\u00e1nyz\u00f3 adatok: pr\u00f3b\u00e1lja \u00fajra k\u00e9s\u0151bb vagy m\u00e1s konfigur\u00e1ci\u00f3val", + "otp_failed": "A k\u00e9tl\u00e9pcs\u0151s azonos\u00edt\u00e1s sikertelen, pr\u00f3b\u00e1lkozzon \u00faj jelsz\u00f3val", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "flow_title": "{name} ({host})", @@ -13,7 +16,8 @@ "2sa": { "data": { "otp_code": "K\u00f3d" - } + }, + "title": "Synology DSM: k\u00e9tl\u00e9pcs\u0151s azonos\u00edt\u00e1s" }, "link": { "data": { @@ -23,8 +27,17 @@ "username": "Felhaszn\u00e1l\u00f3n\u00e9v", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" }, + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}({host})-t?", "title": "Synology DSM" }, + "reauth": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "Indokl\u00e1s: {details}", + "title": "Synology DSM Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "host": "Hoszt", @@ -42,6 +55,7 @@ "step": { "init": { "data": { + "scan_interval": "Percek a vizsg\u00e1latok k\u00f6z\u00f6tt", "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s (m\u00e1sodperc)" } } diff --git a/homeassistant/components/tado/translations/hu.json b/homeassistant/components/tado/translations/hu.json index fd8db27da5e..dfde73ce428 100644 --- a/homeassistant/components/tado/translations/hu.json +++ b/homeassistant/components/tado/translations/hu.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "no_homes": "Ehhez a tado-fi\u00f3khoz nincsenek otthonok.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { @@ -13,7 +14,19 @@ "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "Csatlakozzon Tado-fi\u00f3kj\u00e1hoz" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "fallback": "A tartal\u00e9k m\u00f3d enged\u00e9lyez\u00e9se." + }, + "description": "A tartal\u00e9k m\u00f3d intelligens \u00fctemez\u00e9sre v\u00e1lt a k\u00f6vetkez\u0151 \u00fctemez\u00e9s kapcsol\u00f3n\u00e1l, miut\u00e1n manu\u00e1lisan be\u00e1ll\u00edtotta a z\u00f3n\u00e1t.", + "title": "\u00c1ll\u00edtsa be a Tado-t." } } } diff --git a/homeassistant/components/tesla/translations/hu.json b/homeassistant/components/tesla/translations/hu.json index a4622ce7efa..75a93566df5 100644 --- a/homeassistant/components/tesla/translations/hu.json +++ b/homeassistant/components/tesla/translations/hu.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "MFA k\u00f3d (opcion\u00e1lis)", "password": "Jelsz\u00f3", "username": "E-mail" }, @@ -24,6 +25,7 @@ "step": { "init": { "data": { + "enable_wake_on_start": "Az aut\u00f3k \u00e9bred\u00e9sre k\u00e9nyszer\u00edt\u00e9se ind\u00edt\u00e1skor", "scan_interval": "Szkennel\u00e9sek k\u00f6z\u00f6tti m\u00e1sodpercek" } } diff --git a/homeassistant/components/toon/translations/hu.json b/homeassistant/components/toon/translations/hu.json index 6371bf4c6fd..28a987a4512 100644 --- a/homeassistant/components/toon/translations/hu.json +++ b/homeassistant/components/toon/translations/hu.json @@ -1,11 +1,21 @@ { "config": { "abort": { + "already_configured": "A kiv\u00e1lasztott meg\u00e1llapod\u00e1s m\u00e1r konfigur\u00e1lva van.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", "no_agreements": "Ennek a fi\u00f3knak nincsenek Toon kijelz\u0151i.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", "unknown_authorize_url_generation": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n." + }, + "step": { + "agreement": { + "data": { + "agreement": "Meg\u00e1llapod\u00e1s" + }, + "description": "V\u00e1lassza ki a hozz\u00e1adni k\u00edv\u00e1nt szerz\u0151d\u00e9sc\u00edmet.", + "title": "V\u00e1lassza ki a meg\u00e1llapod\u00e1st" + } } } } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/hu.json b/homeassistant/components/totalconnect/translations/hu.json index e9e991d81d4..319611fd2b1 100644 --- a/homeassistant/components/totalconnect/translations/hu.json +++ b/homeassistant/components/totalconnect/translations/hu.json @@ -25,7 +25,8 @@ "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "Total Connect" } } } diff --git a/homeassistant/components/traccar/translations/hu.json b/homeassistant/components/traccar/translations/hu.json index c4fc027d059..94fc9198921 100644 --- a/homeassistant/components/traccar/translations/hu.json +++ b/homeassistant/components/traccar/translations/hu.json @@ -6,6 +6,12 @@ }, "create_entry": { "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Traccar-ban. \n\n Haszn\u00e1lja a k\u00f6vetkez\u0151 URL-t: `{webhook_url}`\n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t]({docs_url})." + }, + "step": { + "user": { + "description": "Biztosan be\u00e1ll\u00edtja a Traccar szolg\u00e1ltat\u00e1st?", + "title": "A Traccar be\u00e1ll\u00edt\u00e1sa" + } } } } \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/ca.json b/homeassistant/components/tractive/translations/ca.json new file mode 100644 index 00000000000..4854e13a199 --- /dev/null +++ b/homeassistant/components/tractive/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "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/tractive/translations/en.json b/homeassistant/components/tractive/translations/en.json index 4abfd682903..c85034b0729 100644 --- a/homeassistant/components/tractive/translations/en.json +++ b/homeassistant/components/tractive/translations/en.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "email": "E-Mail", + "email": "Email", "password": "Password" } } diff --git a/homeassistant/components/tractive/translations/et.json b/homeassistant/components/tractive/translations/et.json new file mode 100644 index 00000000000..7e9ab892ed4 --- /dev/null +++ b/homeassistant/components/tractive/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "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/tractive/translations/hu.json b/homeassistant/components/tractive/translations/hu.json new file mode 100644 index 00000000000..8830cb61711 --- /dev/null +++ b/homeassistant/components/tractive/translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "Ismeretlen hiba" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Jelsz\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/it.json b/homeassistant/components/tractive/translations/it.json new file mode 100644 index 00000000000..484d1e229e2 --- /dev/null +++ b/homeassistant/components/tractive/translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "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/tractive/translations/pl.json b/homeassistant/components/tractive/translations/pl.json new file mode 100644 index 00000000000..da4e71dc1b7 --- /dev/null +++ b/homeassistant/components/tractive/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "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/tractive/translations/ru.json b/homeassistant/components/tractive/translations/ru.json new file mode 100644 index 00000000000..155e3a99ba5 --- /dev/null +++ b/homeassistant/components/tractive/translations/ru.json @@ -0,0 +1,19 @@ +{ + "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": { + "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/tractive/translations/zh-Hant.json b/homeassistant/components/tractive/translations/zh-Hant.json new file mode 100644 index 00000000000..64aba47b6b8 --- /dev/null +++ b/homeassistant/components/tractive/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "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/transmission/translations/hu.json b/homeassistant/components/transmission/translations/hu.json index 22d4e18df5e..5c968b21ed7 100644 --- a/homeassistant/components/transmission/translations/hu.json +++ b/homeassistant/components/transmission/translations/hu.json @@ -28,7 +28,8 @@ "limit": "Limit", "order": "Sorrend", "scan_interval": "Friss\u00edt\u00e9si gyakoris\u00e1g" - } + }, + "title": "Adja meg az Transmission be\u00e1ll\u00edt\u00e1sokat" } } } diff --git a/homeassistant/components/twentemilieu/translations/hu.json b/homeassistant/components/twentemilieu/translations/hu.json index df83a29ec22..637dadb5baf 100644 --- a/homeassistant/components/twentemilieu/translations/hu.json +++ b/homeassistant/components/twentemilieu/translations/hu.json @@ -4,14 +4,18 @@ "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_address": "A c\u00edm nem tal\u00e1lhat\u00f3 a Twente Milieu szolg\u00e1ltat\u00e1si ter\u00fcleten." }, "step": { "user": { "data": { + "house_letter": "H\u00e1zlev\u00e9l/kieg\u00e9sz\u00edt\u0151", "house_number": "h\u00e1zsz\u00e1m", "post_code": "ir\u00e1ny\u00edt\u00f3sz\u00e1m" - } + }, + "description": "\u00c1ll\u00edtsa be a Twente Milieu szolg\u00e1ltat\u00e1st, amely hullad\u00e9kgy\u0171jt\u00e9si inform\u00e1ci\u00f3kat biztos\u00edt a c\u00edm\u00e9re.", + "title": "Twente Milieu" } } } diff --git a/homeassistant/components/unifi/translations/hu.json b/homeassistant/components/unifi/translations/hu.json index 5c174e9939d..22904c8ec7b 100644 --- a/homeassistant/components/unifi/translations/hu.json +++ b/homeassistant/components/unifi/translations/hu.json @@ -7,7 +7,8 @@ }, "error": { "faulty_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "service_unavailable": "Sikertelen csatlakoz\u00e1s" + "service_unavailable": "Sikertelen csatlakoz\u00e1s", + "unknown_client_mac": "Nincs el\u00e9rhet\u0151 \u00fcgyf\u00e9l ezen a MAC-c\u00edmen" }, "flow_title": "{site} ({host})", "step": { @@ -28,18 +29,46 @@ "step": { "client_control": { "data": { - "dpi_restrictions": "Enged\u00e9lyezze a DPI restrikci\u00f3s csoportok vez\u00e9rl\u00e9s\u00e9t" + "block_client": "H\u00e1l\u00f3zathozz\u00e1f\u00e9r\u00e9s vez\u00e9relt \u00fcgyfelek", + "dpi_restrictions": "Enged\u00e9lyezze a DPI restrikci\u00f3s csoportok vez\u00e9rl\u00e9s\u00e9t", + "poe_clients": "Enged\u00e9lyezze az \u00fcgyfelek POE-vez\u00e9rl\u00e9s\u00e9t" }, - "description": "Konfigur\u00e1lja a klienseket\n\n Hozzon l\u00e9tre kapcsol\u00f3kat azokhoz a sorsz\u00e1mokhoz, amelyeknek vez\u00e9relni k\u00edv\u00e1nja a h\u00e1l\u00f3zati hozz\u00e1f\u00e9r\u00e9st." + "description": "Konfigur\u00e1lja a klienseket\n\n Hozzon l\u00e9tre kapcsol\u00f3kat azokhoz a sorsz\u00e1mokhoz, amelyeknek vez\u00e9relni k\u00edv\u00e1nja a h\u00e1l\u00f3zati hozz\u00e1f\u00e9r\u00e9st.", + "title": "UniFi lehet\u0151s\u00e9gek 2/3" + }, + "device_tracker": { + "data": { + "detection_time": "Id\u0151 m\u00e1sodpercben az utols\u00f3 l\u00e1t\u00e1st\u00f3l a t\u00e1vol tart\u00e1sig", + "ignore_wired_bug": "Az UniFi vezet\u00e9kes hibalogika letilt\u00e1sa", + "ssid_filter": "V\u00e1lassza ki az SSID -ket a vezet\u00e9k n\u00e9lk\u00fcli \u00fcgyfelek nyomon k\u00f6vet\u00e9s\u00e9hez", + "track_clients": "K\u00f6vesse nyomon a h\u00e1l\u00f3zati \u00fcgyfeleket", + "track_devices": "H\u00e1l\u00f3zati eszk\u00f6z\u00f6k nyomon k\u00f6vet\u00e9se (Ubiquiti eszk\u00f6z\u00f6k)", + "track_wired_clients": "Vegyen fel vezet\u00e9kes h\u00e1l\u00f3zati \u00fcgyfeleket" + }, + "description": "Eszk\u00f6zk\u00f6vet\u00e9s konfigur\u00e1l\u00e1sa", + "title": "UniFi lehet\u0151s\u00e9gek 1/3" + }, + "init": { + "data": { + "one": "\u00dcres", + "other": "\u00dcres" + } }, "simple_options": { + "data": { + "block_client": "H\u00e1l\u00f3zathozz\u00e1f\u00e9r\u00e9s vez\u00e9relt \u00fcgyfelek", + "track_clients": "K\u00f6vesse nyomon a h\u00e1l\u00f3zati \u00fcgyfeleket", + "track_devices": "H\u00e1l\u00f3zati eszk\u00f6z\u00f6k nyomon k\u00f6vet\u00e9se (Ubiquiti eszk\u00f6z\u00f6k)" + }, "description": "UniFi integr\u00e1ci\u00f3 konfigur\u00e1l\u00e1sa" }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "S\u00e1vsz\u00e9less\u00e9g-haszn\u00e1lati \u00e9rz\u00e9kel\u0151k l\u00e9trehoz\u00e1sa a h\u00e1l\u00f3zati \u00fcgyfelek sz\u00e1m\u00e1ra", "allow_uptime_sensors": "\u00dczemid\u0151-\u00e9rz\u00e9kel\u0151k h\u00e1l\u00f3zati \u00fcgyfelek sz\u00e1m\u00e1ra" - } + }, + "description": "Statisztikai \u00e9rz\u00e9kel\u0151k konfigur\u00e1l\u00e1sa", + "title": "UniFi lehet\u0151s\u00e9gek 3/3" } } } diff --git a/homeassistant/components/upb/translations/hu.json b/homeassistant/components/upb/translations/hu.json index b09f497a0e4..58b81af7be8 100644 --- a/homeassistant/components/upb/translations/hu.json +++ b/homeassistant/components/upb/translations/hu.json @@ -5,13 +5,18 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_upb_file": "Hi\u00e1nyz\u00f3 vagy \u00e9rv\u00e9nytelen UPB UPStart export f\u00e1jl, ellen\u0151rizze a f\u00e1jl nev\u00e9t \u00e9s el\u00e9r\u00e9si \u00fatj\u00e1t.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { "user": { "data": { + "address": "C\u00edm (l\u00e1sd a fenti le\u00edr\u00e1st)", + "file_path": "Az UPStart UPB exportf\u00e1jl el\u00e9r\u00e9si \u00fatja \u00e9s neve.", "protocol": "Protokoll" - } + }, + "description": "Csatlakoztasson egy univerz\u00e1lis Powerline Bus Powerline Interface modult (UPB PIM). A c\u00edmsornak a \u201etcp\u201d \u201ec\u00edm [: port]\u201d form\u00e1tum\u00fanak kell lennie. A port nem k\u00f6telez\u0151, \u00e9s alap\u00e9rtelmezett \u00e9rt\u00e9ke 2101. P\u00e9lda: '192.168.1.42'. A soros protokollhoz a c\u00edmnek 'tty [: baud]' form\u00e1tum\u00fanak kell lennie. A baud opcion\u00e1lis, \u00e9s alap\u00e9rtelmezett \u00e9rt\u00e9ke 4800. P\u00e9lda: '/dev/ttyS1'.", + "title": "Csatlakoz\u00e1s az UPB PIM-hez" } } } diff --git a/homeassistant/components/upnp/translations/hu.json b/homeassistant/components/upnp/translations/hu.json index 49756babc8b..8ef3ff8dcc0 100644 --- a/homeassistant/components/upnp/translations/hu.json +++ b/homeassistant/components/upnp/translations/hu.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "incomplete_discovery": "Hi\u00e1nyos felfedez\u00e9s", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" }, "error": { @@ -10,8 +11,16 @@ }, "flow_title": "{name}", "step": { + "init": { + "one": "\u00dcres", + "other": "" + }, + "ssdp_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani ezt az UPnP/IGD eszk\u00f6zt?" + }, "user": { "data": { + "scan_interval": "Friss\u00edt\u00e9si intervallum (m\u00e1sodperc, minimum 30)", "unique_id": "Eszk\u00f6z", "usn": "Eszk\u00f6z" } diff --git a/homeassistant/components/uptimerobot/translations/ca.json b/homeassistant/components/uptimerobot/translations/ca.json new file mode 100644 index 00000000000..ee0d2416cc6 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja ha estat configurat", + "unknown": "Error inesperat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_api_key": "Clau API inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/de.json b/homeassistant/components/uptimerobot/translations/de.json new file mode 100644 index 00000000000..81a9960b69c --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/en.json b/homeassistant/components/uptimerobot/translations/en.json index 99ab9426006..d23431fa888 100644 --- a/homeassistant/components/uptimerobot/translations/en.json +++ b/homeassistant/components/uptimerobot/translations/en.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Account already configured", + "already_configured": "Account is already configured", "unknown": "Unexpected error" }, "error": { diff --git a/homeassistant/components/uptimerobot/translations/et.json b/homeassistant/components/uptimerobot/translations/et.json new file mode 100644 index 00000000000..a0608c5fff6 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba h\u00e4\u00e4lestatud", + "unknown": "Ootamatu t\u00f5rge" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_api_key": "Vigane API v\u00f5ti", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/he.json b/homeassistant/components/uptimerobot/translations/he.json new file mode 100644 index 00000000000..5b6fc485e04 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/he.json @@ -0,0 +1,18 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/hu.json b/homeassistant/components/uptimerobot/translations/hu.json new file mode 100644 index 00000000000..b9e14001679 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "unknown": "V\u00e1ratlan hiba" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni", + "invalid_api_key": "\u00c9rv\u00e9nytelen API-kulcs", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/it.json b/homeassistant/components/uptimerobot/translations/it.json new file mode 100644 index 00000000000..6b151199afe --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/pl.json b/homeassistant/components/uptimerobot/translations/pl.json new file mode 100644 index 00000000000..ac413226e98 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_api_key": "Nieprawid\u0142owy klucz API", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/ru.json b/homeassistant/components/uptimerobot/translations/ru.json new file mode 100644 index 00000000000..60e7e8530d1 --- /dev/null +++ b/homeassistant/components/uptimerobot/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.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "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_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/zh-Hant.json b/homeassistant/components/uptimerobot/translations/zh-Hant.json new file mode 100644 index 00000000000..c100c6868b9 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_api_key": "API \u5bc6\u9470\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/translations/hu.json b/homeassistant/components/velbus/translations/hu.json index 414ee7e60c6..6bf3ba689f3 100644 --- a/homeassistant/components/velbus/translations/hu.json +++ b/homeassistant/components/velbus/translations/hu.json @@ -6,6 +6,15 @@ "error": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "name": "Ennek a velbus kapcsolatnak a neve", + "port": "Kapcsolati karakterl\u00e1nc" + }, + "title": "Hat\u00e1rozza meg a velbus kapcsolat t\u00edpus\u00e1t" + } } } } \ No newline at end of file diff --git a/homeassistant/components/vera/translations/hu.json b/homeassistant/components/vera/translations/hu.json new file mode 100644 index 00000000000..1f1e22b9ed8 --- /dev/null +++ b/homeassistant/components/vera/translations/hu.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "cannot_connect": "Nem siker\u00fclt csatlakozni a {base_url}" + }, + "step": { + "user": { + "data": { + "exclude": "Vera eszk\u00f6zazonos\u00edt\u00f3k kiz\u00e1r\u00e1sa a HomeAssistantb\u00f3l.", + "lights": "A Vera kapcsol\u00f3eszk\u00f6z-azonos\u00edt\u00f3k f\u00e9nyk\u00e9nt kezelhet\u0151k a HomeAssistant alkalmaz\u00e1sban.", + "vera_controller_url": "Vez\u00e9rl\u0151 URL" + }, + "description": "Adja meg a Vera vez\u00e9rl\u0151 URL-j\u00e9t al\u00e1bb. Ennek \u00edgy kell kin\u00e9znie: http://192.168.1.161:3480.", + "title": "Vera vez\u00e9rl\u0151 be\u00e1ll\u00edt\u00e1sa" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "exclude": "Vera eszk\u00f6zazonos\u00edt\u00f3k kiz\u00e1r\u00e1sa a HomeAssistantb\u00f3l.", + "lights": "A Vera kapcsol\u00f3eszk\u00f6z-azonos\u00edt\u00f3k f\u00e9nyk\u00e9nt kezelhet\u0151k a HomeAssistant alkalmaz\u00e1sban." + }, + "description": "Az opcion\u00e1lis param\u00e9terekr\u0151l a vera dokument\u00e1ci\u00f3j\u00e1ban olvashat: https://www.home-assistant.io/integrations/vera/. Megjegyz\u00e9s: Az itt v\u00e9grehajtott v\u00e1ltoztat\u00e1sokhoz \u00fajra kell ind\u00edtani a h\u00e1zi asszisztens szervert. Az \u00e9rt\u00e9kek t\u00f6rl\u00e9s\u00e9hez adjon meg egy sz\u00f3k\u00f6zt.", + "title": "Vera vez\u00e9rl\u0151 opci\u00f3k" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/translations/hu.json b/homeassistant/components/vilfo/translations/hu.json index 34db9cf7cc9..4e2ab47a476 100644 --- a/homeassistant/components/vilfo/translations/hu.json +++ b/homeassistant/components/vilfo/translations/hu.json @@ -14,6 +14,7 @@ "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", "host": "Hoszt" }, + "description": "\u00c1ll\u00edtsa be a Vilfo Router integr\u00e1ci\u00f3t. Sz\u00fcks\u00e9ge van a Vilfo Router gazdag\u00e9pnev\u00e9re/IP -c\u00edm\u00e9re \u00e9s egy API hozz\u00e1f\u00e9r\u00e9si jogkivonatra. Ha tov\u00e1bbi inform\u00e1ci\u00f3ra van sz\u00fcks\u00e9ge az integr\u00e1ci\u00f3r\u00f3l \u00e9s a r\u00e9szletekr\u0151l, l\u00e1togasson el a k\u00f6vetkez\u0151 webhelyre: https://www.home-assistant.io/integrations/vilfo", "title": "Csatlakoz\u00e1s a Vilfo routerhez" } } diff --git a/homeassistant/components/vizio/translations/hu.json b/homeassistant/components/vizio/translations/hu.json index 6f0962509f5..edc91cdb31c 100644 --- a/homeassistant/components/vizio/translations/hu.json +++ b/homeassistant/components/vizio/translations/hu.json @@ -6,16 +6,25 @@ "updated_entry": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban defini\u00e1lt n\u00e9v, appok \u00e9s/vagy be\u00e1ll\u00edt\u00e1sok nem egyeznek meg a kor\u00e1bban import\u00e1lt konfigur\u00e1ci\u00f3val, \u00edgy a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00fclt." }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "complete_pairing_failed": "Nem siker\u00fclt befejezni a p\u00e1ros\u00edt\u00e1st. Az \u00fajb\u00f3li elk\u00fcld\u00e9s el\u0151tt gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a megadott PIN-k\u00f3d helyes, a TV tov\u00e1bbra is be van kapcsolva, \u00e9s csatlakozik a h\u00e1l\u00f3zathoz.", + "existing_config_entry_found": "Egy megl\u00e9v\u0151 VIZIO SmartCast Eszk\u00f6z konfigur\u00e1ci\u00f3s bejegyz\u00e9s ugyanazzal a sorozatsz\u00e1mmal m\u00e1r konfigur\u00e1lva van. Ennek konfigur\u00e1l\u00e1s\u00e1hoz t\u00f6r\u00f6lnie kell a megl\u00e9v\u0151 bejegyz\u00e9st." }, "step": { "pair_tv": { "data": { "pin": "PIN-k\u00f3d" - } + }, + "description": "A TV-nek k\u00f3dot kell megjelen\u00edtenie. \u00cdrja be ezt a k\u00f3dot az \u0171rlapba, majd folytassa a k\u00f6vetkez\u0151 l\u00e9p\u00e9ssel a p\u00e1ros\u00edt\u00e1s befejez\u00e9s\u00e9hez.", + "title": "V\u00e9gezze el a p\u00e1ros\u00edt\u00e1si folyamatot" }, "pairing_complete": { - "description": "A VIZIO SmartCast Eszk\u00f6z mostant\u00f3l csatlakozik a Home Assistant-hoz." + "description": "A VIZIO SmartCast Eszk\u00f6z mostant\u00f3l csatlakozik a Home Assistant-hoz.", + "title": "P\u00e1ros\u00edt\u00e1s k\u00e9sz" + }, + "pairing_complete_import": { + "description": "A VIZIO SmartCast Eszk\u00f6z mostant\u00f3l csatlakozik a Home Assistant szolg\u00e1ltat\u00e1shoz. \n\n A Hozz\u00e1f\u00e9r\u00e9si token a \u201e** {access_token} **\u201d.", + "title": "P\u00e1ros\u00edt\u00e1s k\u00e9sz" }, "user": { "data": { @@ -24,6 +33,7 @@ "host": "Hoszt", "name": "N\u00e9v" }, + "description": "A Hozz\u00e1f\u00e9r\u00e9si token csak t\u00e9v\u00e9khez sz\u00fcks\u00e9ges. Ha TV -t konfigur\u00e1l, \u00e9s m\u00e9g nincs Hozz\u00e1f\u00e9r\u00e9si token , hagyja \u00fcresen a p\u00e1ros\u00edt\u00e1si folyamathoz.", "title": "VIZIO SmartCast Eszk\u00f6z" } } @@ -32,8 +42,11 @@ "step": { "init": { "data": { + "apps_to_include_or_exclude": "Alkalmaz\u00e1sok felv\u00e9telre vagy kiz\u00e1r\u00e1sra", + "include_or_exclude": "Alkalmaz\u00e1sok felv\u00e9tele vagy kiz\u00e1r\u00e1sa?", "volume_step": "Hanger\u0151 l\u00e9p\u00e9s nagys\u00e1ga" }, + "description": "Ha rendelkezik Smart TV-vel, opcion\u00e1lisan sz\u0171rheti a forr\u00e1slist\u00e1t \u00fagy, hogy kiv\u00e1lasztja, mely alkalmaz\u00e1sokat k\u00edv\u00e1nja felvenni vagy kiz\u00e1rni a forr\u00e1slist\u00e1b\u00f3l.", "title": "VIZIO SmartCast Eszk\u00f6z be\u00e1ll\u00edt\u00e1sok friss\u00edt\u00e9se" } } diff --git a/homeassistant/components/wemo/translations/hu.json b/homeassistant/components/wemo/translations/hu.json index bcb2f438353..ff9f4dc5f75 100644 --- a/homeassistant/components/wemo/translations/hu.json +++ b/homeassistant/components/wemo/translations/hu.json @@ -3,6 +3,11 @@ "abort": { "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "confirm": { + "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Wemo-t?" + } } }, "device_automation": { diff --git a/homeassistant/components/wiffi/translations/hu.json b/homeassistant/components/wiffi/translations/hu.json index c623f6ddaba..902fabcbc85 100644 --- a/homeassistant/components/wiffi/translations/hu.json +++ b/homeassistant/components/wiffi/translations/hu.json @@ -1,13 +1,15 @@ { "config": { "abort": { + "addr_in_use": "A szerverport m\u00e1r haszn\u00e1latban van.", "start_server_failed": "A szerver ind\u00edt\u00e1sa nem siker\u00fclt." }, "step": { "user": { "data": { "port": "Port" - } + }, + "title": "TCP szerver be\u00e1ll\u00edt\u00e1sa WIFFI eszk\u00f6z\u00f6kh\u00f6z" } } }, diff --git a/homeassistant/components/withings/translations/hu.json b/homeassistant/components/withings/translations/hu.json index ec8c628a485..e26cff027fc 100644 --- a/homeassistant/components/withings/translations/hu.json +++ b/homeassistant/components/withings/translations/hu.json @@ -25,6 +25,7 @@ "title": "Felhaszn\u00e1l\u00f3i profil." }, "reauth": { + "description": "A \u201e{profile}\u201d profilt \u00fajra hiteles\u00edteni kell, hogy tov\u00e1bbra is fogadni tudja a Withings adatokat.", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" } } diff --git a/homeassistant/components/wolflink/translations/hu.json b/homeassistant/components/wolflink/translations/hu.json index c7bb483155d..79d03d91034 100644 --- a/homeassistant/components/wolflink/translations/hu.json +++ b/homeassistant/components/wolflink/translations/hu.json @@ -12,13 +12,15 @@ "device": { "data": { "device_name": "Eszk\u00f6z" - } + }, + "title": "V\u00e1lassza ki a WOLF eszk\u00f6zt" }, "user": { "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "WOLF SmartSet kapcsolat" } } } diff --git a/homeassistant/components/wolflink/translations/sensor.hu.json b/homeassistant/components/wolflink/translations/sensor.hu.json index b393660f35a..34f54e80ae8 100644 --- a/homeassistant/components/wolflink/translations/sensor.hu.json +++ b/homeassistant/components/wolflink/translations/sensor.hu.json @@ -1,9 +1,83 @@ { "state": { "wolflink__state": { + "1_x_warmwasser": "1 x DHW", + "abgasklappe": "F\u00fcstg\u00e1zcsillap\u00edt\u00f3", + "aktiviert": "Aktiv\u00e1lt", + "antilegionellenfunktion": "Anti-legionella funkci\u00f3", + "at_abschaltung": "OT le\u00e1ll\u00edt\u00e1s", + "at_frostschutz": "OT fagyv\u00e9delem", + "aus": "Letiltva", + "auto": "Automatikus", "auto_off_cool": "AutomataKiH\u0171t\u00e9s", + "auto_on_cool": "AutomatikusH\u0171t\u00e9s", "automatik_aus": "Automatikus kikapcsol\u00e1s", - "permanent": "\u00c1lland\u00f3" + "automatik_ein": "Automatikus bekapcsol\u00e1s", + "bereit_keine_ladung": "K\u00e9sz, nincs bet\u00f6ltve", + "betrieb_ohne_brenner": "Munka \u00e9g\u0151 n\u00e9lk\u00fcl", + "cooling": "H\u0171t\u00e9s", + "deaktiviert": "Inakt\u00edv", + "dhw_prior": "DHW Priorit\u00e1s", + "eco": "Takar\u00e9kos", + "ein": "Enged\u00e9lyezve", + "externe_deaktivierung": "K\u00fcls\u0151 deaktiv\u00e1l\u00e1s", + "fernschalter_ein": "T\u00e1vir\u00e1ny\u00edt\u00f3 enged\u00e9lyezve", + "frost_heizkreis": "F\u0171t\u0151k\u00f6r fagy\u00e1s", + "frost_warmwasser": "DHW fagy", + "frostschutz": "Fagyv\u00e9delem", + "gasdruck": "G\u00e1znyom\u00e1s", + "glt_betrieb": "BMS m\u00f3d", + "gradienten_uberwachung": "\u00c1tmenet monitoroz\u00e1s", + "heizbetrieb": "F\u0171t\u00e9si m\u00f3d", + "heizgerat_mit_speicher": "Kaz\u00e1n hengerrel", + "heizung": "F\u0171t\u00e9s", + "initialisierung": "Inicializ\u00e1l\u00e1s", + "kalibration": "Kalibr\u00e1ci\u00f3", + "kalibration_heizbetrieb": "F\u0171t\u00e9si m\u00f3d kalibr\u00e1l\u00e1sa", + "kalibration_kombibetrieb": "Kombin\u00e1lt m\u00f3d kalibr\u00e1l\u00e1sa", + "kalibration_warmwasserbetrieb": "DHW kalibr\u00e1l\u00e1s", + "kaskadenbetrieb": "Kaszk\u00e1d m\u0171k\u00f6d\u00e9s", + "kombibetrieb": "Kombin\u00e1lt m\u00f3d", + "kombigerat": "Kombin\u00e1lt kaz\u00e1n", + "kombigerat_mit_solareinbindung": "Kombin\u00e1lt kaz\u00e1n napelemes integr\u00e1ci\u00f3val", + "mindest_kombizeit": "Minim\u00e1lis kombin\u00e1lt id\u0151", + "nachlauf_heizkreispumpe": "A f\u0171t\u0151k\u00f6r szivatty\u00fa bej\u00e1rat\u00e1sa", + "nachspulen": "Ut\u00f3\u00f6bl\u00edt\u00e9s", + "nur_heizgerat": "Csak kaz\u00e1n", + "parallelbetrieb": "P\u00e1rhuzamos \u00fczemm\u00f3d", + "partymodus": "Party m\u00f3d", + "perm_cooling": "\u00c1lland\u00f3H\u0171t\u00e9s", + "permanent": "\u00c1lland\u00f3", + "permanentbetrieb": "\u00c1lland\u00f3 \u00fczemm\u00f3d", + "reduzierter_betrieb": "Korl\u00e1tozott m\u00f3d", + "rt_abschaltung": "RT le\u00e1ll\u00edt\u00e1s", + "rt_frostschutz": "RT fagyv\u00e9delem", + "ruhekontakt": "Pihen\u0151 kapcsolat", + "schornsteinfeger": "Emisszi\u00f3s vizsg\u00e1lat", + "smart_grid": "SmartGrid", + "smart_home": "OkosOtthon", + "softstart": "L\u00e1gy ind\u00edt\u00e1s", + "solarbetrieb": "Napenergia \u00fczemm\u00f3d", + "sparbetrieb": "Gazdas\u00e1gos m\u00f3d", + "sparen": "Gazdas\u00e1gos", + "spreizung_hoch": "dT t\u00fal sz\u00e9les", + "spreizung_kf": "Spread KF", + "stabilisierung": "Stabiliz\u00e1ci\u00f3", + "standby": "K\u00e9szenl\u00e9t", + "start": "Indul\u00e1s", + "storung": "Hiba", + "taktsperre": "Anti-ciklus", + "telefonfernschalter": "Telefonos t\u00e1vkapcsol\u00f3", + "test": "Teszt", + "tpw": "TPW", + "urlaubsmodus": "Nyaral\u00e1s \u00fczemm\u00f3d", + "ventilprufung": "Szelep teszt", + "warmwasser": "DHW", + "warmwasser_schnellstart": "DHW gyorsind\u00edt\u00e1s", + "warmwasserbetrieb": "DHW m\u00f3d", + "warmwassernachlauf": "DHW befut\u00e1s", + "warmwasservorrang": "DHW priorit\u00e1s", + "zunden": "Gy\u00fajt\u00e1s" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.ca.json b/homeassistant/components/xiaomi_miio/translations/select.ca.json new file mode 100644 index 00000000000..bc96de04645 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.ca.json @@ -0,0 +1,9 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "bright": "Brillant", + "dim": "Atenua", + "off": "OFF" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.he.json b/homeassistant/components/xiaomi_miio/translations/select.he.json index 0059da60e86..2cffbc3b457 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.he.json +++ b/homeassistant/components/xiaomi_miio/translations/select.he.json @@ -1,6 +1,8 @@ { "state": { "xiaomi_miio__led_brightness": { + "bright": "\u05d1\u05d4\u05d9\u05e8", + "dim": "\u05de\u05e2\u05d5\u05de\u05e2\u05dd", "off": "\u05db\u05d1\u05d5\u05d9" } } diff --git a/homeassistant/components/xiaomi_miio/translations/select.hu.json b/homeassistant/components/xiaomi_miio/translations/select.hu.json new file mode 100644 index 00000000000..4e6df2b4a33 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.hu.json @@ -0,0 +1,9 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "bright": "F\u00e9nyes", + "dim": "Hom\u00e1lyos", + "off": "Ki" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.it.json b/homeassistant/components/xiaomi_miio/translations/select.it.json new file mode 100644 index 00000000000..21e79e41e99 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.it.json @@ -0,0 +1,9 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "bright": "Brillante", + "dim": "Fioca", + "off": "Spento" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/hu.json b/homeassistant/components/yale_smart_alarm/translations/hu.json new file mode 100644 index 00000000000..8c60574227d --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/hu.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "Ter\u00fclet ID", + "name": "N\u00e9v", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + }, + "user": { + "data": { + "area_id": "Ter\u00fclet ID", + "name": "N\u00e9v", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/hu.json b/homeassistant/components/youless/translations/hu.json new file mode 100644 index 00000000000..21c7a7ebe4b --- /dev/null +++ b/homeassistant/components/youless/translations/hu.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni" + }, + "step": { + "user": { + "data": { + "host": "H\u00e1zigazda", + "name": "N\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index 2b078092ed7..9722095b548 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -8,13 +8,27 @@ }, "flow_title": "{name}", "step": { + "pick_radio": { + "data": { + "radio_type": "R\u00e1di\u00f3 t\u00edpusa" + }, + "description": "V\u00e1lassza ki a Zigbee r\u00e1di\u00f3 t\u00edpus\u00e1t", + "title": "R\u00e1di\u00f3 t\u00edpusa" + }, "port_config": { "data": { - "baudrate": "port sebess\u00e9g" + "baudrate": "port sebess\u00e9g", + "flow_control": "adat\u00e1raml\u00e1s szab\u00e1lyoz\u00e1sa", + "path": "Soros eszk\u00f6z el\u00e9r\u00e9si \u00fatja" }, + "description": "Adja meg a port specifikus be\u00e1ll\u00edt\u00e1sokat", "title": "Be\u00e1ll\u00edt\u00e1sok" }, "user": { + "data": { + "path": "Soros eszk\u00f6z el\u00e9r\u00e9si \u00fatja" + }, + "description": "V\u00e1lassza ki a Zigbee r\u00e1di\u00f3 soros portj\u00e1t", "title": "ZHA" } } @@ -35,11 +49,59 @@ } }, "device_automation": { + "action_type": { + "squawk": "Riaszt\u00e1s", + "warn": "Figyelmeztet\u00e9s" + }, "trigger_subtype": { - "turn_off": "Kikapcsol\u00e1s" + "both_buttons": "Mindk\u00e9t gomb", + "button_1": "Els\u0151 gomb", + "button_2": "M\u00e1sodik gomb", + "button_3": "Harmadik gomb", + "button_4": "Negyedik gomb", + "button_5": "\u00d6t\u00f6dik gomb", + "button_6": "Hatodik gomb", + "close": "Bez\u00e1r\u00e1s", + "dim_down": "S\u00f6t\u00e9t\u00edt", + "dim_up": "Vil\u00e1gos\u00edt", + "face_1": "aktiv\u00e1lt 1 arccal", + "face_2": "aktiv\u00e1lt 2 arccal", + "face_3": "aktiv\u00e1lt 3 arccal", + "face_4": "aktiv\u00e1lt 4 arccal", + "face_5": "aktiv\u00e1lt 5 arccal", + "face_6": "aktiv\u00e1lt 6 arccal", + "face_any": "B\u00e1rmely/meghat\u00e1rozott arc(ok) aktiv\u00e1l\u00e1s\u00e1val", + "left": "Bal", + "open": "Nyitva", + "right": "Jobb", + "turn_off": "Kikapcsol\u00e1s", + "turn_on": "Bekapcsol\u00e1s" }, "trigger_type": { - "device_offline": "Eszk\u00f6z offline" + "device_dropped": "A k\u00e9sz\u00fcl\u00e9k eldobva", + "device_flipped": "Eszk\u00f6z \u00e1tford\u00edtva \"{subtype}\"", + "device_knocked": "Az eszk\u00f6zt le\u00fct\u00f6tt\u00e9k \"{subtype}\"", + "device_offline": "Eszk\u00f6z offline", + "device_rotated": "Eszk\u00f6z elforgatva \"{subtype}\"", + "device_shaken": "A k\u00e9sz\u00fcl\u00e9k megr\u00e1zk\u00f3dott", + "device_slid": "Eszk\u00f6z cs\u00fasztatott \"{subtype}\"", + "device_tilted": "K\u00e9sz\u00fcl\u00e9k megd\u00f6ntve", + "remote_button_alt_double_press": "A \u201e{subtype}\u201d gombra dupl\u00e1n kattintva (Alternat\u00edv m\u00f3d)", + "remote_button_alt_long_press": "\"{subtype}\" gomb folyamatosan nyomva (alternat\u00edv m\u00f3d)", + "remote_button_alt_long_release": "A \u201e{subtype}\u201d gomb elenged\u00e9se hossz\u00fa megnyom\u00e1st k\u00f6vet\u0151en (alternat\u00edv m\u00f3d)", + "remote_button_alt_quadruple_press": "A \u201e{subtype}\u201d gombra n\u00e9gyszer kattintottak (alternat\u00edv m\u00f3d)", + "remote_button_alt_quintuple_press": "\"{subtype}\" gombra \u00f6tsz\u00f6r kattintottak (alternat\u00edv m\u00f3d)", + "remote_button_alt_short_press": "\u201e{subtype}\u201d gomb lenyomva (alternat\u00edv m\u00f3d)", + "remote_button_alt_short_release": "A \"{subtype}\" gomb elengedett (alternat\u00edv m\u00f3d)", + "remote_button_alt_triple_press": "A \u201e{subtype}\u201d gombra h\u00e1romszor kattintottak (alternat\u00edv m\u00f3d)", + "remote_button_double_press": "\"{subtype}\" gombra k\u00e9tszer kattintottak", + "remote_button_long_press": "A \"{subtype}\" gomb folyamatosan lenyomva", + "remote_button_long_release": "A \"{subtype}\" gomb hossz\u00fa megnyom\u00e1s ut\u00e1n elengedve", + "remote_button_quadruple_press": "\"{subtype}\" gombra n\u00e9gyszer kattintottak", + "remote_button_quintuple_press": "\"{subtype}\" gombra \u00f6tsz\u00f6r kattintottak", + "remote_button_short_press": "\"{subtype}\" gomb lenyomva", + "remote_button_short_release": "\"{subtype}\" gomb elengedve", + "remote_button_triple_press": "\"{subtype}\" gombra h\u00e1romszor kattintottak" } } } \ No newline at end of file From 58ccfff067e430f898ab22aac6e772060de513b7 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 6 Aug 2021 05:23:05 +0300 Subject: [PATCH 002/355] Fix Shelly last_reset (#54101) --- homeassistant/components/shelly/entity.py | 1 - homeassistant/components/shelly/sensor.py | 52 ++++++++++++++++------- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index a1ce2e671d1..0d23f5abffc 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -285,7 +285,6 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): self._unit: None | str | Callable[[dict], str] = unit self._unique_id: str = f"{super().unique_id}-{self.attribute}" self._name = get_entity_name(wrapper.device, block, self.description.name) - self._last_value: str | None = None @property def unique_id(self) -> str: diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 56e4f63bc75..07e4f4a4fe3 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -1,9 +1,12 @@ """Sensor for Shelly.""" from __future__ import annotations -from datetime import datetime +from datetime import timedelta +import logging from typing import Final, cast +import aioshelly + from homeassistant.components import sensor from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry @@ -23,6 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util import dt +from . import ShellyDeviceWrapper from .const import LAST_RESET_NEVER, LAST_RESET_UPTIME, SHAIR_MAX_WORK_HOURS from .entity import ( BlockAttributeDescription, @@ -35,6 +39,8 @@ from .entity import ( ) from .utils import get_device_uptime, temperature_unit +_LOGGER: Final = logging.getLogger(__name__) + SENSORS: Final = { ("device", "battery"): BlockAttributeDescription( name="Battery", @@ -255,9 +261,39 @@ async def async_setup_entry( class ShellySensor(ShellyBlockAttributeEntity, SensorEntity): """Represent a shelly sensor.""" + def __init__( + self, + wrapper: ShellyDeviceWrapper, + block: aioshelly.Block, + attribute: str, + description: BlockAttributeDescription, + ) -> None: + """Initialize sensor.""" + super().__init__(wrapper, block, attribute, description) + self._last_value: float | None = None + + if description.last_reset == LAST_RESET_NEVER: + self._attr_last_reset = dt.utc_from_timestamp(0) + elif description.last_reset == LAST_RESET_UPTIME: + self._attr_last_reset = ( + dt.utcnow() - timedelta(seconds=wrapper.device.status["uptime"]) + ).replace(second=0, microsecond=0) + @property def state(self) -> StateType: """Return value of sensor.""" + if ( + self.description.last_reset == LAST_RESET_UPTIME + and self.attribute_value is not None + ): + value = cast(float, self.attribute_value) + + if self._last_value and self._last_value > value: + self._attr_last_reset = dt.utcnow().replace(second=0, microsecond=0) + _LOGGER.info("Energy reset detected for entity %s", self.name) + + self._last_value = value + return self.attribute_value @property @@ -265,20 +301,6 @@ class ShellySensor(ShellyBlockAttributeEntity, SensorEntity): """State class of sensor.""" return self.description.state_class - @property - def last_reset(self) -> datetime | None: - """State class of sensor.""" - if self.description.last_reset == LAST_RESET_UPTIME: - self._last_value = get_device_uptime( - self.wrapper.device.status, self._last_value - ) - return dt.parse_datetime(self._last_value) - - if self.description.last_reset == LAST_RESET_NEVER: - return dt.utc_from_timestamp(0) - - return None - @property def unit_of_measurement(self) -> str | None: """Return unit of sensor.""" From 46ad55455b240e91a2b053a56d522ca9921e29be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Aug 2021 21:24:09 -0500 Subject: [PATCH 003/355] Bump zeroconf to 0.33.3 (#54108) --- 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 ee1e9a8e1ab..7b3cfa1fefd 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.33.2"], + "requirements": ["zeroconf==0.33.3"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cc4bb9d72ac..d435f165f61 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ sqlalchemy==1.4.17 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 -zeroconf==0.33.2 +zeroconf==0.33.3 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 187351dace9..3aec770dc6b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2442,7 +2442,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.33.2 +zeroconf==0.33.3 # homeassistant.components.zha zha-quirks==0.0.59 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e673479b616..10794fe2c94 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1347,7 +1347,7 @@ youless-api==0.10 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.33.2 +zeroconf==0.33.3 # homeassistant.components.zha zha-quirks==0.0.59 From adc9f7549360159e94a594720cca2b20f5d7625f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Aug 2021 21:24:24 -0500 Subject: [PATCH 004/355] Increase time before scene and script HomeKit entities are reset (#54105) --- homeassistant/components/homekit/type_switches.py | 4 +++- tests/components/homekit/test_type_switches.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 381110a4e79..ef9dadff287 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -57,6 +57,8 @@ VALVE_TYPE = { ACTIVATE_ONLY_SWITCH_DOMAINS = {"scene", "script"} +ACTIVATE_ONLY_RESET_SECONDS = 10 + @TYPES.register("Outlet") class Outlet(HomeAccessory): @@ -141,7 +143,7 @@ class Switch(HomeAccessory): self.async_call_service(self._domain, service, params) if self.activate_only: - async_call_later(self.hass, 1, self.reset_switch) + async_call_later(self.hass, ACTIVATE_ONLY_RESET_SECONDS, self.reset_switch) @callback def async_update_state(self, new_state): diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 455f7a6141a..6df1f0182ed 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -329,7 +329,13 @@ async def test_reset_switch(hass, hk_driver, events): future = dt_util.utcnow() + timedelta(seconds=1) async_fire_time_changed(hass, future) await hass.async_block_till_done() + assert acc.char_on.value is True + + future = dt_util.utcnow() + timedelta(seconds=10) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() assert acc.char_on.value is False + assert len(events) == 1 assert not call_turn_off @@ -367,7 +373,13 @@ async def test_script_switch(hass, hk_driver, events): future = dt_util.utcnow() + timedelta(seconds=1) async_fire_time_changed(hass, future) await hass.async_block_till_done() + assert acc.char_on.value is True + + future = dt_util.utcnow() + timedelta(seconds=10) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() assert acc.char_on.value is False + assert len(events) == 1 assert not call_turn_off From 582f2ae2f605a2ccbd8f565dd5815e771743f2bf Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 6 Aug 2021 04:24:41 +0200 Subject: [PATCH 005/355] Two fixes (#54102) --- homeassistant/components/fritz/sensor.py | 4 ++-- homeassistant/components/fritz/switch.py | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index faf2be23164..d7a34564b43 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -81,12 +81,12 @@ def _retrieve_max_kb_s_received_state(status: FritzStatus, last_value: str) -> f def _retrieve_gb_sent_state(status: FritzStatus, last_value: str) -> float: """Return upload total data.""" - return round(status.bytes_sent * 8 / 1000 / 1000 / 1000, 1) # type: ignore[no-any-return] + return round(status.bytes_sent / 1000 / 1000 / 1000, 1) # type: ignore[no-any-return] def _retrieve_gb_received_state(status: FritzStatus, last_value: str) -> float: """Return download total data.""" - return round(status.bytes_received * 8 / 1000 / 1000 / 1000, 1) # type: ignore[no-any-return] + return round(status.bytes_received / 1000 / 1000 / 1000, 1) # type: ignore[no-any-return] class SensorData(TypedDict, total=False): diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 10eb6553dbd..da17bef7159 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -13,7 +13,6 @@ from fritzconnection.core.exceptions import ( FritzSecurityError, FritzServiceError, ) -import slugify as unicode_slug import xmltodict from homeassistant.components.network import async_get_source_ip @@ -248,10 +247,18 @@ def wifi_entities_list( ) if network_info: ssid = network_info["NewSSID"] - if unicode_slug.slugify(ssid, lowercase=False) in networks.values(): + _LOGGER.debug("SSID from device: <%s>", ssid) + if ( + slugify( + ssid, + ) + in [slugify(v) for v in networks.values()] + ): + _LOGGER.debug("SSID duplicated, adding suffix") networks[i] = f'{ssid} {std_table[network_info["NewStandard"]]}' else: networks[i] = ssid + _LOGGER.debug("SSID normalized: <%s>", networks[i]) return [ FritzBoxWifiSwitch(fritzbox_tools, device_friendly_name, net, network_name) From 02d691816518cd9404e44b0dc8f7791597911892 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Fri, 6 Aug 2021 06:13:47 +0200 Subject: [PATCH 006/355] Run coordinator config_entry_first_refresh in rituals_perfume_genie setup (#54080) --- homeassistant/components/rituals_perfume_genie/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index ee2a517a3f7..8a9ed5d94a3 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -39,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hublot = device.hublot coordinator = RitualsDataUpdateCoordinator(hass, device) - await coordinator.async_refresh() + await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id][DEVICES][hublot] = device hass.data[DOMAIN][entry.entry_id][COORDINATORS][hublot] = coordinator From 8ead20a76b8ab9550f5fb0da4cf2e9cb57b19715 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 6 Aug 2021 06:26:02 +0200 Subject: [PATCH 007/355] Test knx sensor (#54090) --- tests/components/knx/test_sensor.py | 95 +++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 tests/components/knx/test_sensor.py diff --git a/tests/components/knx/test_sensor.py b/tests/components/knx/test_sensor.py new file mode 100644 index 00000000000..16ea5e8d385 --- /dev/null +++ b/tests/components/knx/test_sensor.py @@ -0,0 +1,95 @@ +"""Test KNX sensor.""" +from homeassistant.components.knx.const import CONF_STATE_ADDRESS, CONF_SYNC_STATE +from homeassistant.components.knx.schema import SensorSchema +from homeassistant.const import CONF_NAME, CONF_TYPE, STATE_UNKNOWN +from homeassistant.core import HomeAssistant + +from .conftest import KNXTestKit + +from tests.common import async_capture_events + + +async def test_sensor(hass: HomeAssistant, knx: KNXTestKit): + """Test simple KNX sensor.""" + + await knx.setup_integration( + { + SensorSchema.PLATFORM_NAME: { + CONF_NAME: "test", + CONF_STATE_ADDRESS: "1/1/1", + CONF_TYPE: "current", # 2 byte unsigned int + } + } + ) + assert len(hass.states.async_all()) == 1 + state = hass.states.get("sensor.test") + assert state.state is STATE_UNKNOWN + + # StateUpdater initialize state + await knx.assert_read("1/1/1") + await knx.receive_response("1/1/1", (0, 40)) + state = hass.states.get("sensor.test") + assert state.state == "40" + + # update from KNX + await knx.receive_write("1/1/1", (0x03, 0xE8)) + state = hass.states.get("sensor.test") + assert state.state == "1000" + + # don't answer to GroupValueRead requests + await knx.receive_read("1/1/1") + await knx.assert_no_telegram() + + +async def test_always_callback(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX sensor with always_callback.""" + + events = async_capture_events(hass, "state_changed") + await knx.setup_integration( + { + SensorSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test_normal", + CONF_STATE_ADDRESS: "1/1/1", + CONF_SYNC_STATE: False, + CONF_TYPE: "percentU8", + }, + { + CONF_NAME: "test_always", + CONF_STATE_ADDRESS: "2/2/2", + SensorSchema.CONF_ALWAYS_CALLBACK: True, + CONF_SYNC_STATE: False, + CONF_TYPE: "percentU8", + }, + ] + } + ) + assert len(hass.states.async_all()) == 2 + # state changes form None to "unknown" + assert len(events) == 2 + + # receive initial telegram + await knx.receive_write("1/1/1", (0x42,)) + await knx.receive_write("2/2/2", (0x42,)) + await hass.async_block_till_done() + assert len(events) == 4 + + # receive second telegram with identical payload + # always_callback shall force state_changed event + await knx.receive_write("1/1/1", (0x42,)) + await knx.receive_write("2/2/2", (0x42,)) + await hass.async_block_till_done() + assert len(events) == 5 + + # receive telegram with different payload + await knx.receive_write("1/1/1", (0xFA,)) + await knx.receive_write("2/2/2", (0xFA,)) + await hass.async_block_till_done() + assert len(events) == 7 + + # receive telegram with second payload again + # always_callback shall force state_changed event + await knx.receive_write("1/1/1", (0xFA,)) + await knx.receive_write("2/2/2", (0xFA,)) + await hass.async_block_till_done() + assert len(events) == 8 From ab34ef475eaff94df76c9d2044fb19ef4c2c5c34 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 6 Aug 2021 06:33:20 +0200 Subject: [PATCH 008/355] Test KNX binary sensor (#53820) * test binary_sensor * test binary_sensor with reset_after --- tests/components/knx/test_binary_sensor.py | 205 +++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 tests/components/knx/test_binary_sensor.py diff --git a/tests/components/knx/test_binary_sensor.py b/tests/components/knx/test_binary_sensor.py new file mode 100644 index 00000000000..48b871b85e4 --- /dev/null +++ b/tests/components/knx/test_binary_sensor.py @@ -0,0 +1,205 @@ +"""Test KNX binary sensor.""" +from datetime import timedelta + +from homeassistant.components.knx.const import CONF_STATE_ADDRESS, CONF_SYNC_STATE +from homeassistant.components.knx.schema import BinarySensorSchema +from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.util import dt + +from .conftest import KNXTestKit + +from tests.common import async_capture_events, async_fire_time_changed + + +async def test_binary_sensor(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX binary sensor and inverted binary_sensor.""" + await knx.setup_integration( + { + BinarySensorSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test_normal", + CONF_STATE_ADDRESS: "1/1/1", + }, + { + CONF_NAME: "test_invert", + CONF_STATE_ADDRESS: "2/2/2", + BinarySensorSchema.CONF_INVERT: True, + }, + ] + } + ) + assert len(hass.states.async_all()) == 2 + + # StateUpdater initialize state + await knx.assert_read("1/1/1") + await knx.assert_read("2/2/2") + await knx.receive_response("1/1/1", True) + await knx.receive_response("2/2/2", False) + state_normal = hass.states.get("binary_sensor.test_normal") + state_invert = hass.states.get("binary_sensor.test_invert") + assert state_normal.state is STATE_ON + assert state_invert.state is STATE_ON + + # receive OFF telegram + await knx.receive_write("1/1/1", False) + await knx.receive_write("2/2/2", True) + state_normal = hass.states.get("binary_sensor.test_normal") + state_invert = hass.states.get("binary_sensor.test_invert") + assert state_normal.state is STATE_OFF + assert state_invert.state is STATE_OFF + + # receive ON telegram + await knx.receive_write("1/1/1", True) + await knx.receive_write("2/2/2", False) + state_normal = hass.states.get("binary_sensor.test_normal") + state_invert = hass.states.get("binary_sensor.test_invert") + assert state_normal.state is STATE_ON + assert state_invert.state is STATE_ON + + # binary_sensor does not respond to read + await knx.receive_read("1/1/1") + await knx.receive_read("2/2/2") + await knx.assert_telegram_count(0) + + +async def test_binary_sensor_ignore_internal_state( + hass: HomeAssistant, knx: KNXTestKit +): + """Test KNX binary_sensor with ignore_internal_state.""" + events = async_capture_events(hass, "state_changed") + + await knx.setup_integration( + { + BinarySensorSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test_normal", + CONF_STATE_ADDRESS: "1/1/1", + CONF_SYNC_STATE: False, + }, + { + CONF_NAME: "test_ignore", + CONF_STATE_ADDRESS: "2/2/2", + BinarySensorSchema.CONF_IGNORE_INTERNAL_STATE: True, + CONF_SYNC_STATE: False, + }, + ] + } + ) + assert len(hass.states.async_all()) == 2 + # binary_sensor defaults to STATE_OFF - state change form None + assert len(events) == 2 + + # receive initial ON telegram + await knx.receive_write("1/1/1", True) + await knx.receive_write("2/2/2", True) + await hass.async_block_till_done() + assert len(events) == 4 + + # receive second ON telegram - ignore_internal_state shall force state_changed event + await knx.receive_write("1/1/1", True) + await knx.receive_write("2/2/2", True) + await hass.async_block_till_done() + assert len(events) == 5 + + # receive first OFF telegram + await knx.receive_write("1/1/1", False) + await knx.receive_write("2/2/2", False) + await hass.async_block_till_done() + assert len(events) == 7 + + # receive second OFF telegram - ignore_internal_state shall force state_changed event + await knx.receive_write("1/1/1", False) + await knx.receive_write("2/2/2", False) + await hass.async_block_till_done() + assert len(events) == 8 + + +async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX binary_sensor with context timeout.""" + async_fire_time_changed(hass, dt.utcnow()) + events = async_capture_events(hass, "state_changed") + + await knx.setup_integration( + { + BinarySensorSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test", + CONF_STATE_ADDRESS: "2/2/2", + BinarySensorSchema.CONF_CONTEXT_TIMEOUT: 1, + CONF_SYNC_STATE: False, + }, + ] + } + ) + assert len(hass.states.async_all()) == 1 + assert len(events) == 1 + events.pop() + + # receive initial ON telegram + await knx.receive_write("2/2/2", True) + await hass.async_block_till_done() + # no change yet - still in 1 sec context (additional async_block_till_done needed for time change) + assert len(events) == 0 + state = hass.states.get("binary_sensor.test") + assert state.state is STATE_OFF + assert state.attributes.get("counter") == 0 + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) + await hass.async_block_till_done() + # state changed twice after context timeout - once to ON with counter 1 and once to counter 0 + state = hass.states.get("binary_sensor.test") + assert state.state is STATE_ON + assert state.attributes.get("counter") == 0 + # additional async_block_till_done needed event capture + await hass.async_block_till_done() + assert len(events) == 2 + assert events.pop(0).data.get("new_state").attributes.get("counter") == 1 + assert events.pop(0).data.get("new_state").attributes.get("counter") == 0 + + # receive 2 telegrams in context + await knx.receive_write("2/2/2", True) + await knx.receive_write("2/2/2", True) + assert len(events) == 0 + state = hass.states.get("binary_sensor.test") + assert state.state is STATE_ON + assert state.attributes.get("counter") == 0 + await hass.async_block_till_done() + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.test") + assert state.state is STATE_ON + assert state.attributes.get("counter") == 0 + await hass.async_block_till_done() + assert len(events) == 2 + assert events.pop(0).data.get("new_state").attributes.get("counter") == 2 + assert events.pop(0).data.get("new_state").attributes.get("counter") == 0 + + +async def test_binary_sensor_reset(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX binary_sensor with reset_after function.""" + async_fire_time_changed(hass, dt.utcnow()) + + await knx.setup_integration( + { + BinarySensorSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test", + CONF_STATE_ADDRESS: "2/2/2", + BinarySensorSchema.CONF_RESET_AFTER: 1, + CONF_SYNC_STATE: False, + }, + ] + } + ) + assert len(hass.states.async_all()) == 1 + + # receive ON telegram + await knx.receive_write("2/2/2", True) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.test") + assert state.state is STATE_ON + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) + await hass.async_block_till_done() + # state reset after after timeout + state = hass.states.get("binary_sensor.test") + assert state.state is STATE_OFF From 1cc3ffe20dc4b03d816bf9f68a37bc42e0df2f5d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 Aug 2021 21:41:50 -0700 Subject: [PATCH 009/355] Fix jinja warning (#54109) --- homeassistant/helpers/template.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 66354aa7aa6..08b3956a490 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -22,7 +22,7 @@ from urllib.parse import urlencode as urllib_urlencode import weakref import jinja2 -from jinja2 import contextfunction, pass_context +from jinja2 import pass_context from jinja2.sandbox import ImmutableSandboxedEnvironment from jinja2.utils import Namespace import voluptuous as vol @@ -1521,7 +1521,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): def wrapper(*args, **kwargs): return func(hass, *args[1:], **kwargs) - return contextfunction(wrapper) + return pass_context(wrapper) self.globals["device_entities"] = hassfunction(device_entities) self.filters["device_entities"] = pass_context(self.globals["device_entities"]) From 19adce844cbdd425530df01f1c1d36cdb113cad9 Mon Sep 17 00:00:00 2001 From: Oscar Calvo <2091582+ocalvo@users.noreply.github.com> Date: Fri, 6 Aug 2021 03:18:29 -0700 Subject: [PATCH 010/355] Gracefully handle additional GSM errors (#54114) --- homeassistant/components/sms/gateway.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/sms/gateway.py b/homeassistant/components/sms/gateway.py index 51667ef8f77..5003f7019ca 100644 --- a/homeassistant/components/sms/gateway.py +++ b/homeassistant/components/sms/gateway.py @@ -25,6 +25,10 @@ class Gateway: await self._worker.set_incoming_sms_async() except gammu.ERR_NOTSUPPORTED: _LOGGER.warning("Your phone does not support incoming SMS notifications!") + except gammu.GSMError: + _LOGGER.warning( + "GSM error, your phone does not support incoming SMS notifications!" + ) else: await self._worker.set_incoming_callback_async(self.sms_callback) From 206073632fdd70a92ee6c3c4ad0ba9267da2762e Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 6 Aug 2021 14:59:00 +0200 Subject: [PATCH 011/355] Fix sensor PLATFORM_SCHEMA for ebox and enphase_envoy (#54142) * Fix sensor PLATFORM_SCHEMA * fix pylint --- homeassistant/components/ebox/sensor.py | 4 +++- homeassistant/components/enphase_envoy/sensor.py | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index e27c6fe0772..e98dea45929 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -122,10 +122,12 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), ) +SENSOR_TYPE_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_MONITORED_VARIABLES): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] + cv.ensure_list, [vol.In(SENSOR_TYPE_KEYS)] ), vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 29d273401f4..3af5cd1ec0c 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -1,4 +1,5 @@ """Support for Enphase Envoy solar energy monitor.""" +from __future__ import annotations import logging @@ -22,14 +23,15 @@ 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=list(SENSORS)): vol.All( - cv.ensure_list, [vol.In(list(SENSORS))] + vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( + cv.ensure_list, [vol.In(SENSOR_KEYS)] ), vol.Optional(CONF_NAME, default=""): cv.string, } From 5f790f6bd9178f600da17215048e60d2eccf1788 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Aug 2021 11:15:35 -0500 Subject: [PATCH 012/355] Fetch interface index from network integration instead of socket.if_nametoindex in zeroconf (#54152) --- homeassistant/components/network/models.py | 1 + homeassistant/components/network/util.py | 1 + homeassistant/components/zeroconf/__init__.py | 5 ++- tests/components/network/test_init.py | 32 +++++++++++++++++++ tests/components/zeroconf/test_init.py | 4 +++ 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/network/models.py b/homeassistant/components/network/models.py index a007eb8636d..d3fbc824489 100644 --- a/homeassistant/components/network/models.py +++ b/homeassistant/components/network/models.py @@ -24,6 +24,7 @@ class Adapter(TypedDict): """Configured network adapters.""" name: str + index: int enabled: bool auto: bool default: bool diff --git a/homeassistant/components/network/util.py b/homeassistant/components/network/util.py index eece4b38548..f8b33b3df90 100644 --- a/homeassistant/components/network/util.py +++ b/homeassistant/components/network/util.py @@ -116,6 +116,7 @@ def _ifaddr_adapter_to_ha( return { "name": adapter.nice_name, + "index": adapter.index, "enabled": False, "auto": auto, "default": default, diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 4c4c81aff32..cdb46318578 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -155,9 +155,8 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: for ipv4 in ipv4s if not ipaddress.ip_address(ipv4["address"]).is_loopback ) - if adapter["ipv6"]: - ifi = socket.if_nametoindex(adapter["name"]) - interfaces.append(ifi) + if adapter["ipv6"] and adapter["index"] not in interfaces: + interfaces.append(adapter["index"]) ipv6 = True if not any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters): diff --git a/tests/components/network/test_init.py b/tests/components/network/test_init.py index bc4c543842f..6a85f5ea9e8 100644 --- a/tests/components/network/test_init.py +++ b/tests/components/network/test_init.py @@ -21,15 +21,19 @@ def _generate_mock_adapters(): mock_lo0 = Mock(spec=ifaddr.Adapter) mock_lo0.nice_name = "lo0" mock_lo0.ips = [ifaddr.IP("127.0.0.1", 8, "lo0")] + mock_lo0.index = 0 mock_eth0 = Mock(spec=ifaddr.Adapter) mock_eth0.nice_name = "eth0" mock_eth0.ips = [ifaddr.IP(("2001:db8::", 1, 1), 8, "eth0")] + mock_eth0.index = 1 mock_eth1 = Mock(spec=ifaddr.Adapter) mock_eth1.nice_name = "eth1" mock_eth1.ips = [ifaddr.IP("192.168.1.5", 23, "eth1")] + mock_eth1.index = 2 mock_vtun0 = Mock(spec=ifaddr.Adapter) mock_vtun0.nice_name = "vtun0" mock_vtun0.ips = [ifaddr.IP("169.254.3.2", 16, "vtun0")] + mock_vtun0.index = 3 return [mock_eth0, mock_lo0, mock_eth1, mock_vtun0] @@ -51,6 +55,7 @@ async def test_async_detect_interfaces_setting_non_loopback_route(hass, hass_sto assert network_obj.adapters == [ { "auto": False, + "index": 1, "default": False, "enabled": False, "ipv4": [], @@ -65,6 +70,7 @@ async def test_async_detect_interfaces_setting_non_loopback_route(hass, hass_sto "name": "eth0", }, { + "index": 0, "auto": False, "default": False, "enabled": False, @@ -73,6 +79,7 @@ async def test_async_detect_interfaces_setting_non_loopback_route(hass, hass_sto "name": "lo0", }, { + "index": 2, "auto": True, "default": True, "enabled": True, @@ -81,6 +88,7 @@ async def test_async_detect_interfaces_setting_non_loopback_route(hass, hass_sto "name": "eth1", }, { + "index": 3, "auto": False, "default": False, "enabled": False, @@ -107,6 +115,7 @@ async def test_async_detect_interfaces_setting_loopback_route(hass, hass_storage assert network_obj.configured_adapters == [] assert network_obj.adapters == [ { + "index": 1, "auto": True, "default": False, "enabled": True, @@ -122,6 +131,7 @@ async def test_async_detect_interfaces_setting_loopback_route(hass, hass_storage "name": "eth0", }, { + "index": 0, "auto": False, "default": True, "enabled": False, @@ -130,6 +140,7 @@ async def test_async_detect_interfaces_setting_loopback_route(hass, hass_storage "name": "lo0", }, { + "index": 2, "auto": True, "default": False, "enabled": True, @@ -138,6 +149,7 @@ async def test_async_detect_interfaces_setting_loopback_route(hass, hass_storage "name": "eth1", }, { + "index": 3, "auto": False, "default": False, "enabled": False, @@ -165,6 +177,7 @@ async def test_async_detect_interfaces_setting_empty_route(hass, hass_storage): assert network_obj.adapters == [ { "auto": True, + "index": 1, "default": False, "enabled": True, "ipv4": [], @@ -180,6 +193,7 @@ async def test_async_detect_interfaces_setting_empty_route(hass, hass_storage): }, { "auto": False, + "index": 0, "default": False, "enabled": False, "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], @@ -188,6 +202,7 @@ async def test_async_detect_interfaces_setting_empty_route(hass, hass_storage): }, { "auto": True, + "index": 2, "default": False, "enabled": True, "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], @@ -196,6 +211,7 @@ async def test_async_detect_interfaces_setting_empty_route(hass, hass_storage): }, { "auto": False, + "index": 3, "default": False, "enabled": False, "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], @@ -222,6 +238,7 @@ async def test_async_detect_interfaces_setting_exception(hass, hass_storage): assert network_obj.adapters == [ { "auto": True, + "index": 1, "default": False, "enabled": True, "ipv4": [], @@ -237,6 +254,7 @@ async def test_async_detect_interfaces_setting_exception(hass, hass_storage): }, { "auto": False, + "index": 0, "default": False, "enabled": False, "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], @@ -245,6 +263,7 @@ async def test_async_detect_interfaces_setting_exception(hass, hass_storage): }, { "auto": True, + "index": 2, "default": False, "enabled": True, "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], @@ -253,6 +272,7 @@ async def test_async_detect_interfaces_setting_exception(hass, hass_storage): }, { "auto": False, + "index": 3, "default": False, "enabled": False, "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], @@ -285,6 +305,7 @@ async def test_interfaces_configured_from_storage(hass, hass_storage): assert network_obj.adapters == [ { "auto": False, + "index": 1, "default": False, "enabled": True, "ipv4": [], @@ -300,6 +321,7 @@ async def test_interfaces_configured_from_storage(hass, hass_storage): }, { "auto": False, + "index": 0, "default": False, "enabled": False, "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], @@ -308,6 +330,7 @@ async def test_interfaces_configured_from_storage(hass, hass_storage): }, { "auto": True, + "index": 2, "default": True, "enabled": True, "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], @@ -316,6 +339,7 @@ async def test_interfaces_configured_from_storage(hass, hass_storage): }, { "auto": False, + "index": 3, "default": False, "enabled": True, "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], @@ -356,6 +380,7 @@ async def test_interfaces_configured_from_storage_websocket_update( assert response["result"][ATTR_ADAPTERS] == [ { "auto": False, + "index": 1, "default": False, "enabled": True, "ipv4": [], @@ -371,6 +396,7 @@ async def test_interfaces_configured_from_storage_websocket_update( }, { "auto": False, + "index": 0, "default": False, "enabled": False, "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], @@ -379,6 +405,7 @@ async def test_interfaces_configured_from_storage_websocket_update( }, { "auto": True, + "index": 2, "default": True, "enabled": True, "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], @@ -387,6 +414,7 @@ async def test_interfaces_configured_from_storage_websocket_update( }, { "auto": False, + "index": 3, "default": False, "enabled": True, "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], @@ -407,6 +435,7 @@ async def test_interfaces_configured_from_storage_websocket_update( assert response["result"][ATTR_ADAPTERS] == [ { "auto": False, + "index": 1, "default": False, "enabled": False, "ipv4": [], @@ -422,6 +451,7 @@ async def test_interfaces_configured_from_storage_websocket_update( }, { "auto": False, + "index": 0, "default": False, "enabled": False, "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], @@ -430,6 +460,7 @@ async def test_interfaces_configured_from_storage_websocket_update( }, { "auto": True, + "index": 2, "default": True, "enabled": True, "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], @@ -438,6 +469,7 @@ async def test_interfaces_configured_from_storage_websocket_update( }, { "auto": False, + "index": 3, "default": False, "enabled": False, "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 3b8cf883a13..e1e346621fe 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -725,6 +725,7 @@ async def test_async_detect_interfaces_setting_non_loopback_route( _ADAPTERS_WITH_MANUAL_CONFIG = [ { "auto": True, + "index": 1, "default": False, "enabled": True, "ipv4": [], @@ -746,6 +747,7 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [ }, { "auto": True, + "index": 2, "default": False, "enabled": True, "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], @@ -754,6 +756,7 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [ }, { "auto": True, + "index": 3, "default": False, "enabled": True, "ipv4": [{"address": "172.16.1.5", "network_prefix": 23}], @@ -769,6 +772,7 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [ }, { "auto": False, + "index": 4, "default": False, "enabled": False, "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], From 2db278a7a7ef7042d95fa4af08c159079f81b195 Mon Sep 17 00:00:00 2001 From: Raj Laud <50647620+rajlaud@users.noreply.github.com> Date: Fri, 6 Aug 2021 12:29:52 -0400 Subject: [PATCH 013/355] Fix Squeezebox dhcp discovery (#54137) * Fix Squeezebox dhcp discovery and allow ignore * Test ignoring known Squeezebox players * Fix linter errors --- .../components/squeezebox/browse_media.py | 1 - .../components/squeezebox/config_flow.py | 45 ++++++++++++------- .../components/squeezebox/media_player.py | 9 ++-- .../components/squeezebox/test_config_flow.py | 38 ++++++++++++---- 4 files changed, 62 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index 4c0ec186707..294a1105a71 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -137,7 +137,6 @@ async def build_item_response(entity, player, payload): async def library_payload(player): """Create response payload to describe contents of library.""" - library_info = { "title": "Music Library", "media_class": MEDIA_CLASS_DIRECTORY, diff --git a/homeassistant/components/squeezebox/config_flow.py b/homeassistant/components/squeezebox/config_flow.py index 1f1c23942db..4b05588e281 100644 --- a/homeassistant/components/squeezebox/config_flow.py +++ b/homeassistant/components/squeezebox/config_flow.py @@ -5,8 +5,9 @@ import logging from pysqueezebox import Server, async_discover import voluptuous as vol -from homeassistant import config_entries -from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.dhcp import MAC_ADDRESS +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -15,6 +16,8 @@ from homeassistant.const import ( HTTP_UNAUTHORIZED, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.entity_registry import async_get from .const import DEFAULT_PORT, DOMAIN @@ -166,28 +169,18 @@ class SqueezeboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason=error) return self.async_create_entry(title=config[CONF_HOST], data=config) - async def async_step_discovery(self, discovery_info): - """Handle discovery.""" - _LOGGER.debug("Reached discovery flow with info: %s", discovery_info) + 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) if "uuid" in discovery_info: await self.async_set_unique_id(discovery_info.pop("uuid")) self._abort_if_unique_id_configured() else: # attempt to connect to server and determine uuid. will fail if # password required - - if CONF_HOST not in discovery_info and IP_ADDRESS in discovery_info: - discovery_info[CONF_HOST] = discovery_info[IP_ADDRESS] - - if CONF_PORT not in discovery_info: - discovery_info[CONF_PORT] = DEFAULT_PORT - error = await self._validate_input(discovery_info) if error: - if MAC_ADDRESS in discovery_info: - await self.async_set_unique_id(discovery_info[MAC_ADDRESS]) - else: - await self._async_handle_discovery_without_unique_id() + await self._async_handle_discovery_without_unique_id() # update schema with suggested values from discovery self.data_schema = _base_schema(discovery_info) @@ -195,3 +188,23 @@ class SqueezeboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.context.update({"title_placeholders": {"host": discovery_info[CONF_HOST]}}) return await self.async_step_edit() + + async def async_step_dhcp(self, discovery_info): + """Handle dhcp discovery of a Squeezebox player.""" + _LOGGER.debug( + "Reached dhcp discovery of a player with info: %s", discovery_info + ) + await self.async_set_unique_id(format_mac(discovery_info[MAC_ADDRESS])) + self._abort_if_unique_id_configured() + + _LOGGER.debug("Configuring dhcp player with unique id: %s", self.unique_id) + + registry = async_get(self.hass) + + # if we have detected this player, do nothing. if not, there must be a server out there for us to configure, so start the normal user flow (which tries to autodetect server) + if registry.async_get_entity_id(MP_DOMAIN, DOMAIN, self.unique_id) is not None: + # this player is already known, so do nothing other than mark as configured + raise data_entry_flow.AbortFlow("already_configured") + + # if the player is unknown, then we likely need to configure its server + return await self.async_step_user() diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index baf8a011c65..1ba406097d7 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -27,7 +27,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) -from homeassistant.config_entries import SOURCE_DISCOVERY +from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY from homeassistant.const import ( ATTR_COMMAND, CONF_HOST, @@ -43,6 +43,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -127,7 +128,7 @@ async def start_server_discovery(hass): asyncio.create_task( hass.config_entries.flow.async_init( DOMAIN, - context={"source": SOURCE_DISCOVERY}, + context={"source": SOURCE_INTEGRATION_DISCOVERY}, data={ CONF_HOST: server.host, CONF_PORT: int(server.port), @@ -146,7 +147,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 @@ -283,7 +283,7 @@ class SqueezeBoxEntity(MediaPlayerEntity): @property def unique_id(self): """Return a unique ID.""" - return self._player.player_id + return format_mac(self._player.player_id) @property def available(self): @@ -573,7 +573,6 @@ class SqueezeBoxEntity(MediaPlayerEntity): async def async_browse_media(self, media_content_type=None, media_content_id=None): """Implement the websocket media browsing helper.""" - _LOGGER.debug( "Reached async_browse_media with content_type %s and content_id %s", media_content_type, diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py index e740ea671cd..9460e2235be 100644 --- a/tests/components/squeezebox/test_config_flow.py +++ b/tests/components/squeezebox/test_config_flow.py @@ -178,7 +178,7 @@ async def test_discovery(hass): ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_HOST: HOST, CONF_PORT: PORT, "uuid": UUID}, ) assert result["type"] == RESULT_TYPE_FORM @@ -190,7 +190,7 @@ async def test_discovery_no_uuid(hass): 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_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_HOST: HOST, CONF_PORT: PORT}, ) assert result["type"] == RESULT_TYPE_FORM @@ -199,9 +199,8 @@ async def test_discovery_no_uuid(hass): async def test_dhcp_discovery(hass): """Test we can process discovery from dhcp.""" - with patch( - "pysqueezebox.Server.async_query", - return_value={"uuid": UUID}, + with patch("pysqueezebox.Server.async_query", return_value={"uuid": UUID},), patch( + "homeassistant.components.squeezebox.config_flow.async_discover", mock_discover ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -216,9 +215,12 @@ async def test_dhcp_discovery(hass): assert result["step_id"] == "edit" -async def test_dhcp_discovery_no_connection(hass): - """Test we can process discovery from dhcp without connecting to squeezebox server.""" - with patch("pysqueezebox.Server.async_query", new=patch_async_query_unauthorized): +async def test_dhcp_discovery_no_server_found(hass): + """Test we can handle dhcp discovery when no server is found.""" + with patch( + "homeassistant.components.squeezebox.config_flow.async_discover", + mock_failed_discover, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, @@ -229,7 +231,25 @@ async def test_dhcp_discovery_no_connection(hass): }, ) assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "edit" + assert result["step_id"] == "user" + + +async def test_dhcp_discovery_existing_player(hass): + """Test that we properly ignore known players during dhcp discover.""" + with patch( + "homeassistant.helpers.entity_registry.EntityRegistry.async_get_entity_id", + return_value="test_entity", + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + IP_ADDRESS: "1.1.1.1", + MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", + HOSTNAME: "any", + }, + ) + assert result["type"] == RESULT_TYPE_ABORT async def test_import(hass): From d842fc288fce35f90222528682e783ec5cba0597 Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Fri, 6 Aug 2021 17:34:21 +0100 Subject: [PATCH 014/355] Ignore Coinbase vault wallets (#54133) * Exclude vault balances * Update option flow validation * Update test name * Add missed check * Fix dangerous default --- .../components/coinbase/config_flow.py | 8 +++- homeassistant/components/coinbase/const.py | 2 + homeassistant/components/coinbase/sensor.py | 16 ++++++-- tests/components/coinbase/common.py | 10 ++--- tests/components/coinbase/const.py | 14 ++++++- tests/components/coinbase/test_config_flow.py | 4 +- tests/components/coinbase/test_init.py | 41 +++++++++++++++---- 7 files changed, 74 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/coinbase/config_flow.py b/homeassistant/components/coinbase/config_flow.py index 4ea36dad266..5901aeeed9a 100644 --- a/homeassistant/components/coinbase/config_flow.py +++ b/homeassistant/components/coinbase/config_flow.py @@ -14,6 +14,8 @@ from . import get_accounts from .const import ( API_ACCOUNT_CURRENCY, API_RATES, + API_RESOURCE_TYPE, + API_TYPE_VAULT, CONF_CURRENCIES, CONF_EXCHANGE_BASE, CONF_EXCHANGE_RATES, @@ -65,7 +67,11 @@ async def validate_options( accounts = await hass.async_add_executor_job(get_accounts, client) - accounts_currencies = [account[API_ACCOUNT_CURRENCY] for account in accounts] + accounts_currencies = [ + account[API_ACCOUNT_CURRENCY] + for account in accounts + if account[API_RESOURCE_TYPE] != API_TYPE_VAULT + ] available_rates = await hass.async_add_executor_job(client.get_exchange_rates) if CONF_CURRENCIES in options: for currency in options[CONF_CURRENCIES]: diff --git a/homeassistant/components/coinbase/const.py b/homeassistant/components/coinbase/const.py index a7ed0b15986..dc2922d1531 100644 --- a/homeassistant/components/coinbase/const.py +++ b/homeassistant/components/coinbase/const.py @@ -18,6 +18,8 @@ API_ACCOUNT_NATIVE_BALANCE = "native_balance" API_ACCOUNT_NAME = "name" API_ACCOUNTS_DATA = "data" API_RATES = "rates" +API_RESOURCE_TYPE = "type" +API_TYPE_VAULT = "vault" WALLETS = { "1INCH": "1INCH", diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index c86f21bac1d..f836a604f6a 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -12,6 +12,8 @@ from .const import ( API_ACCOUNT_NAME, API_ACCOUNT_NATIVE_BALANCE, API_RATES, + API_RESOURCE_TYPE, + API_TYPE_VAULT, CONF_CURRENCIES, CONF_EXCHANGE_RATES, DOMAIN, @@ -41,7 +43,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] provided_currencies = [ - account[API_ACCOUNT_CURRENCY] for account in instance.accounts + account[API_ACCOUNT_CURRENCY] + for account in instance.accounts + if account[API_RESOURCE_TYPE] != API_TYPE_VAULT ] desired_currencies = [] @@ -82,7 +86,10 @@ class AccountSensor(SensorEntity): self._coinbase_data = coinbase_data self._currency = currency for account in coinbase_data.accounts: - if account[API_ACCOUNT_CURRENCY] == currency: + if ( + account[API_ACCOUNT_CURRENCY] == currency + and account[API_RESOURCE_TYPE] != API_TYPE_VAULT + ): self._name = f"Coinbase {account[API_ACCOUNT_NAME]}" self._id = ( f"coinbase-{account[API_ACCOUNT_ID]}-wallet-" @@ -135,7 +142,10 @@ class AccountSensor(SensorEntity): """Get the latest state of the sensor.""" self._coinbase_data.update() for account in self._coinbase_data.accounts: - if account[API_ACCOUNT_CURRENCY] == self._currency: + if ( + account[API_ACCOUNT_CURRENCY] == self._currency + and account[API_RESOURCE_TYPE] != API_TYPE_VAULT + ): self._state = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT] self._native_balance = account[API_ACCOUNT_NATIVE_BALANCE][ API_ACCOUNT_AMOUNT diff --git a/tests/components/coinbase/common.py b/tests/components/coinbase/common.py index 5fcab6605bd..231a5128585 100644 --- a/tests/components/coinbase/common.py +++ b/tests/components/coinbase/common.py @@ -6,7 +6,7 @@ from homeassistant.components.coinbase.const import ( ) from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN -from .const import GOOD_EXCHNAGE_RATE, GOOD_EXCHNAGE_RATE_2, MOCK_ACCOUNTS_RESPONSE +from .const import GOOD_EXCHANGE_RATE, GOOD_EXCHANGE_RATE_2, MOCK_ACCOUNTS_RESPONSE from tests.common import MockConfigEntry @@ -60,11 +60,11 @@ def mock_get_exchange_rates(): """Return a heavily reduced mock list of exchange rates for testing.""" return { "currency": "USD", - "rates": {GOOD_EXCHNAGE_RATE_2: "0.109", GOOD_EXCHNAGE_RATE: "0.00002"}, + "rates": {GOOD_EXCHANGE_RATE_2: "0.109", GOOD_EXCHANGE_RATE: "0.00002"}, } -async def init_mock_coinbase(hass): +async def init_mock_coinbase(hass, currencies=None, rates=None): """Init Coinbase integration for testing.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -72,8 +72,8 @@ async def init_mock_coinbase(hass): title="Test User", data={CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"}, options={ - CONF_CURRENCIES: [], - CONF_EXCHANGE_RATES: [], + CONF_CURRENCIES: currencies or [], + CONF_EXCHANGE_RATES: rates or [], }, ) config_entry.add_to_hass(hass) diff --git a/tests/components/coinbase/const.py b/tests/components/coinbase/const.py index 7d36d0be9a7..082c986aa59 100644 --- a/tests/components/coinbase/const.py +++ b/tests/components/coinbase/const.py @@ -3,8 +3,8 @@ GOOD_CURRENCY = "BTC" GOOD_CURRENCY_2 = "USD" GOOD_CURRENCY_3 = "EUR" -GOOD_EXCHNAGE_RATE = "BTC" -GOOD_EXCHNAGE_RATE_2 = "ATOM" +GOOD_EXCHANGE_RATE = "BTC" +GOOD_EXCHANGE_RATE_2 = "ATOM" BAD_CURRENCY = "ETH" BAD_EXCHANGE_RATE = "ETH" @@ -15,6 +15,15 @@ MOCK_ACCOUNTS_RESPONSE = [ "id": "123456789", "name": "BTC Wallet", "native_balance": {"amount": "100.12", "currency": GOOD_CURRENCY_2}, + "type": "wallet", + }, + { + "balance": {"amount": "100.00", "currency": GOOD_CURRENCY}, + "currency": GOOD_CURRENCY, + "id": "abcdefg", + "name": "BTC Vault", + "native_balance": {"amount": "100.12", "currency": GOOD_CURRENCY_2}, + "type": "vault", }, { "balance": {"amount": "9.90", "currency": GOOD_CURRENCY_2}, @@ -22,5 +31,6 @@ MOCK_ACCOUNTS_RESPONSE = [ "id": "987654321", "name": "USD Wallet", "native_balance": {"amount": "9.90", "currency": GOOD_CURRENCY_2}, + "type": "fiat", }, ] diff --git a/tests/components/coinbase/test_config_flow.py b/tests/components/coinbase/test_config_flow.py index d153cecc249..fa13648ee71 100644 --- a/tests/components/coinbase/test_config_flow.py +++ b/tests/components/coinbase/test_config_flow.py @@ -19,7 +19,7 @@ from .common import ( mock_get_exchange_rates, mocked_get_accounts, ) -from .const import BAD_CURRENCY, BAD_EXCHANGE_RATE, GOOD_CURRENCY, GOOD_EXCHNAGE_RATE +from .const import BAD_CURRENCY, BAD_EXCHANGE_RATE, GOOD_CURRENCY, GOOD_EXCHANGE_RATE from tests.common import MockConfigEntry @@ -160,7 +160,7 @@ async def test_option_form(hass): result["flow_id"], user_input={ CONF_CURRENCIES: [GOOD_CURRENCY], - CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE], + CONF_EXCHANGE_RATES: [GOOD_EXCHANGE_RATE], }, ) assert result2["type"] == "create_entry" diff --git a/tests/components/coinbase/test_init.py b/tests/components/coinbase/test_init.py index 36f0ff95472..efb5ba85f73 100644 --- a/tests/components/coinbase/test_init.py +++ b/tests/components/coinbase/test_init.py @@ -3,6 +3,7 @@ from unittest.mock import patch from homeassistant import config_entries from homeassistant.components.coinbase.const import ( + API_TYPE_VAULT, CONF_CURRENCIES, CONF_EXCHANGE_RATES, CONF_YAML_API_TOKEN, @@ -22,8 +23,8 @@ from .common import ( from .const import ( GOOD_CURRENCY, GOOD_CURRENCY_2, - GOOD_EXCHNAGE_RATE, - GOOD_EXCHNAGE_RATE_2, + GOOD_EXCHANGE_RATE, + GOOD_EXCHANGE_RATE_2, ) @@ -34,7 +35,7 @@ async def test_setup(hass): CONF_API_KEY: "123456", CONF_YAML_API_TOKEN: "AbCDeF", CONF_CURRENCIES: [GOOD_CURRENCY, GOOD_CURRENCY_2], - CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE, GOOD_EXCHNAGE_RATE_2], + CONF_EXCHANGE_RATES: [GOOD_EXCHANGE_RATE, GOOD_EXCHANGE_RATE_2], } } with patch( @@ -54,7 +55,7 @@ async def test_setup(hass): assert entries[0].source == config_entries.SOURCE_IMPORT assert entries[0].options == { CONF_CURRENCIES: [GOOD_CURRENCY, GOOD_CURRENCY_2], - CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE, GOOD_EXCHNAGE_RATE_2], + CONF_EXCHANGE_RATES: [GOOD_EXCHANGE_RATE, GOOD_EXCHANGE_RATE_2], } @@ -103,7 +104,7 @@ async def test_option_updates(hass: HomeAssistant): result["flow_id"], user_input={ CONF_CURRENCIES: [GOOD_CURRENCY, GOOD_CURRENCY_2], - CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE, GOOD_EXCHNAGE_RATE_2], + CONF_EXCHANGE_RATES: [GOOD_EXCHANGE_RATE, GOOD_EXCHANGE_RATE_2], }, ) await hass.async_block_till_done() @@ -126,7 +127,7 @@ async def test_option_updates(hass: HomeAssistant): ] assert currencies == [GOOD_CURRENCY, GOOD_CURRENCY_2] - assert rates == [GOOD_EXCHNAGE_RATE, GOOD_EXCHNAGE_RATE_2] + assert rates == [GOOD_EXCHANGE_RATE, GOOD_EXCHANGE_RATE_2] result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() @@ -134,7 +135,7 @@ async def test_option_updates(hass: HomeAssistant): result["flow_id"], user_input={ CONF_CURRENCIES: [GOOD_CURRENCY], - CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE], + CONF_EXCHANGE_RATES: [GOOD_EXCHANGE_RATE], }, ) await hass.async_block_till_done() @@ -157,4 +158,28 @@ async def test_option_updates(hass: HomeAssistant): ] assert currencies == [GOOD_CURRENCY] - assert rates == [GOOD_EXCHNAGE_RATE] + assert rates == [GOOD_EXCHANGE_RATE] + + +async def test_ignore_vaults_wallets(hass: HomeAssistant): + """Test vaults are ignored in wallet sensors.""" + + with patch( + "coinbase.wallet.client.Client.get_current_user", + return_value=mock_get_current_user(), + ), patch( + "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts + ), patch( + "coinbase.wallet.client.Client.get_exchange_rates", + return_value=mock_get_exchange_rates(), + ): + config_entry = await init_mock_coinbase(hass, currencies=[GOOD_CURRENCY]) + await hass.async_block_till_done() + + registry = entity_registry.async_get(hass) + entities = entity_registry.async_entries_for_config_entry( + registry, config_entry.entry_id + ) + assert len(entities) == 1 + entity = entities[0] + assert API_TYPE_VAULT not in entity.original_name.lower() From 483a4535c8659bdf79199fee86fd66fc548b1301 Mon Sep 17 00:00:00 2001 From: Niccolo Zapponi Date: Fri, 6 Aug 2021 17:34:42 +0100 Subject: [PATCH 015/355] Handle software version being None when setting up HomeKit accessories (#54130) * Convert all HomeKit service info to string prior to checking for max length * Added check for None software version * Added test case for numeric version number * Update tests/components/homekit/test_accessories.py Co-authored-by: J. Nick Koston * Fix style & none version test * Fix test * revert other change since it should be covered by the format_sw_version fix Co-authored-by: J. Nick Koston --- .../components/homekit/accessories.py | 3 +- tests/components/homekit/test_accessories.py | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index c1f5078e2d6..c3fac44486c 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -238,9 +238,10 @@ class HomeAccessory(Accessory): model = self.config[ATTR_MODEL] else: model = domain.title() + sw_version = None if self.config.get(ATTR_SW_VERSION) is not None: sw_version = format_sw_version(self.config[ATTR_SW_VERSION]) - else: + if sw_version is None: sw_version = __version__ self.set_info_service( diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 5904d1c11c6..975864b42d5 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -133,6 +133,39 @@ async def test_home_accessory(hass, hk_driver): ) assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == "0.4.3" + acc4 = HomeAccessory( + hass, + hk_driver, + "Home Accessory that exceeds the maximum maximum maximum maximum maximum maximum length", + entity_id2, + 3, + { + ATTR_MODEL: "Awesome Model that exceeds the maximum maximum maximum maximum maximum maximum length", + ATTR_MANUFACTURER: "Lux Brands that exceeds the maximum maximum maximum maximum maximum maximum length", + ATTR_SW_VERSION: "will_not_match_regex", + ATTR_INTEGRATION: "luxe that exceeds the maximum maximum maximum maximum maximum maximum length", + }, + ) + assert acc4.available is False + serv = acc4.services[0] # SERV_ACCESSORY_INFO + assert ( + serv.get_characteristic(CHAR_NAME).value + == "Home Accessory that exceeds the maximum maximum maximum maximum " + ) + assert ( + serv.get_characteristic(CHAR_MANUFACTURER).value + == "Lux Brands that exceeds the maximum maximum maximum maximum maxi" + ) + assert ( + serv.get_characteristic(CHAR_MODEL).value + == "Awesome Model that exceeds the maximum maximum maximum maximum m" + ) + assert ( + serv.get_characteristic(CHAR_SERIAL_NUMBER).value + == "light.accessory_that_exceeds_the_maximum_maximum_maximum_maximum" + ) + assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == hass_version + hass.states.async_set(entity_id, "on") await hass.async_block_till_done() with patch( From ddbd4558271fad4dfa24935f2eddeac287e91c99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zoe=20=E2=9C=A8?= <87827343+zoeisnowooze@users.noreply.github.com> Date: Fri, 6 Aug 2021 12:56:27 -0400 Subject: [PATCH 016/355] Add statistics support for the PVOutput sensor (#54149) --- homeassistant/components/pvoutput/sensor.py | 42 ++++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 5744dbfff9a..305615e4b2c 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -1,7 +1,10 @@ """Support for getting collected information from PVOutput.""" +from __future__ import annotations + from collections import namedtuple -from datetime import timedelta +from datetime import datetime, timedelta import logging +from typing import cast import voluptuous as vol @@ -9,6 +12,7 @@ from homeassistant.components.rest.data import RestData from homeassistant.components.sensor import ( DEVICE_CLASS_ENERGY, PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, SensorEntity, ) from homeassistant.const import ( @@ -22,6 +26,8 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) _ENDPOINT = "http://pvoutput.org/service/r2/getstatus.jsp" @@ -68,12 +74,15 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([PvoutputSensor(rest, name)]) -class PvoutputSensor(SensorEntity): +class PvoutputSensor(SensorEntity, RestoreEntity): """Representation of a PVOutput sensor.""" + _attr_state_class = STATE_CLASS_MEASUREMENT _attr_device_class = DEVICE_CLASS_ENERGY _attr_unit_of_measurement = ENERGY_WATT_HOUR + _old_state: int | None = None + def __init__(self, rest, name): """Initialize a PVOutput sensor.""" self.rest = rest @@ -120,8 +129,37 @@ class PvoutputSensor(SensorEntity): await self.rest.async_update() self._async_update_from_rest_data() + new_state: int | None = None + state = cast("str | None", self.state) + if state is not None: + new_state = int(state) + + did_reset = False + if new_state is None: + did_reset = False + elif self._old_state is None: + did_reset = True + elif new_state == 0: + did_reset = self._old_state != 0 + elif new_state < self._old_state: + did_reset = True + + if did_reset: + self._attr_last_reset = dt_util.utcnow() + + if new_state is not None: + self._old_state = new_state + async def async_added_to_hass(self): """Ensure the data from the initial update is reflected in the state.""" + last_state = await self.async_get_last_state() + if last_state is not None: + if "last_reset" in last_state.attributes: + self._attr_last_reset = dt_util.as_utc( + datetime.fromisoformat(last_state.attributes["last_reset"]) + ) + self._old_state = int(last_state.state) + self._async_update_from_rest_data() @callback From 13e7cd237eacfa2d9b33e4d225de90236b12882c Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 6 Aug 2021 18:56:51 +0200 Subject: [PATCH 017/355] Convert to using sensor descriptors (#54115) --- homeassistant/components/rfxtrx/__init__.py | 46 ---- .../components/rfxtrx/binary_sensor.py | 60 +++-- homeassistant/components/rfxtrx/sensor.py | 233 ++++++++++++++---- 3 files changed, 232 insertions(+), 107 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 44e1d537408..34b7c01600a 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -1,7 +1,6 @@ """Support for RFXtrx devices.""" import asyncio import binascii -from collections import OrderedDict import copy import functools import logging @@ -22,20 +21,7 @@ from homeassistant.const import ( CONF_DEVICES, CONF_HOST, CONF_PORT, - DEGREE, - ELECTRIC_CURRENT_AMPERE, - ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, EVENT_HOMEASSISTANT_STOP, - LENGTH_MILLIMETERS, - PERCENTAGE, - POWER_WATT, - PRECIPITATION_MILLIMETERS_PER_HOUR, - PRESSURE_HPA, - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - SPEED_METERS_PER_SECOND, - TEMP_CELSIUS, - UV_INDEX, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -66,38 +52,6 @@ DEFAULT_SIGNAL_REPETITIONS = 1 SIGNAL_EVENT = f"{DOMAIN}_event" -DATA_TYPES = OrderedDict( - [ - ("Temperature", TEMP_CELSIUS), - ("Temperature2", TEMP_CELSIUS), - ("Humidity", PERCENTAGE), - ("Barometer", PRESSURE_HPA), - ("Wind direction", DEGREE), - ("Rain rate", PRECIPITATION_MILLIMETERS_PER_HOUR), - ("Energy usage", POWER_WATT), - ("Total usage", ENERGY_KILO_WATT_HOUR), - ("Sound", None), - ("Sensor Status", None), - ("Counter value", "count"), - ("UV", UV_INDEX), - ("Humidity status", None), - ("Forecast", None), - ("Forecast numeric", None), - ("Rain total", LENGTH_MILLIMETERS), - ("Wind average speed", SPEED_METERS_PER_SECOND), - ("Wind gust", SPEED_METERS_PER_SECOND), - ("Chill", TEMP_CELSIUS), - ("Count", "count"), - ("Current Ch. 1", ELECTRIC_CURRENT_AMPERE), - ("Current Ch. 2", ELECTRIC_CURRENT_AMPERE), - ("Current Ch. 3", ELECTRIC_CURRENT_AMPERE), - ("Voltage", ELECTRIC_POTENTIAL_VOLT), - ("Current", ELECTRIC_CURRENT_AMPERE), - ("Battery numeric", PERCENTAGE), - ("Rssi numeric", SIGNAL_STRENGTH_DECIBELS_MILLIWATT), - ] -) - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 9e3d24cdb6a..d697c56f7e8 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -1,4 +1,7 @@ """Support for RFXtrx binary sensors.""" +from __future__ import annotations + +from dataclasses import replace import logging import RFXtrx as rfxtrxmod @@ -7,6 +10,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOTION, DEVICE_CLASS_SMOKE, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.const import ( CONF_COMMAND_OFF, @@ -51,13 +55,30 @@ SENSOR_STATUS_OFF = [ "Normal Tamper", ] -DEVICE_TYPE_DEVICE_CLASS = { - "X10 Security Motion Detector": DEVICE_CLASS_MOTION, - "KD101 Smoke Detector": DEVICE_CLASS_SMOKE, - "Visonic Powercode Motion Detector": DEVICE_CLASS_MOTION, - "Alecto SA30 Smoke Detector": DEVICE_CLASS_SMOKE, - "RM174RF Smoke Detector": DEVICE_CLASS_SMOKE, -} +SENSOR_TYPES = ( + BinarySensorEntityDescription( + key="X10 Security Motion Detector", + device_class=DEVICE_CLASS_MOTION, + ), + BinarySensorEntityDescription( + key="KD101 Smoke Detector", + device_class=DEVICE_CLASS_SMOKE, + ), + BinarySensorEntityDescription( + key="Visonic Powercode Motion Detector", + device_class=DEVICE_CLASS_MOTION, + ), + BinarySensorEntityDescription( + key="Alecto SA30 Smoke Detector", + device_class=DEVICE_CLASS_SMOKE, + ), + BinarySensorEntityDescription( + key="RM174RF Smoke Detector", + device_class=DEVICE_CLASS_SMOKE, + ), +) + +SENSOR_TYPES_DICT = {desc.key: desc for desc in SENSOR_TYPES} def supported(event): @@ -85,6 +106,14 @@ async def async_setup_entry( discovery_info = config_entry.data + def get_sensor_description(type_string: str, device_class: str | None = None): + description = SENSOR_TYPES_DICT.get(type_string) + if description is None: + description = BinarySensorEntityDescription(key=type_string) + if device_class: + description = replace(description, device_class=device) + return description + for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): event = get_rfx_object(packet_id) if event is None: @@ -107,9 +136,8 @@ async def async_setup_entry( device = RfxtrxBinarySensor( event.device, device_id, - entity_info.get( - CONF_DEVICE_CLASS, - DEVICE_TYPE_DEVICE_CLASS.get(event.device.type_string), + get_sensor_description( + event.device.type_string, entity_info.get(CONF_DEVICE_CLASS) ), entity_info.get(CONF_OFF_DELAY), entity_info.get(CONF_DATA_BITS), @@ -137,11 +165,12 @@ async def async_setup_entry( event.device.subtype, "".join(f"{x:02x}" for x in event.data), ) + sensor = RfxtrxBinarySensor( event.device, device_id, event=event, - device_class=DEVICE_TYPE_DEVICE_CLASS.get(event.device.type_string), + entity_description=get_sensor_description(event.device.type_string), ) async_add_entities([sensor]) @@ -156,7 +185,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): self, device, device_id, - device_class=None, + entity_description, off_delay=None, data_bits=None, cmd_on=None, @@ -165,7 +194,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): ): """Initialize the RFXtrx sensor.""" super().__init__(device, device_id, event=event) - self._device_class = device_class + self.entity_description = entity_description self._data_bits = data_bits self._off_delay = off_delay self._state = None @@ -190,11 +219,6 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): """We should force updates. Repeated states have meaning.""" return True - @property - def device_class(self): - """Return the sensor class.""" - return self._device_class - @property def is_on(self): """Return true if the sensor state is True.""" diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 72cd9f6bbf6..8b9d5e5c389 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -1,5 +1,9 @@ """Support for RFXtrx sensors.""" +from __future__ import annotations + +from dataclasses import dataclass import logging +from typing import Callable from RFXtrx import ControlEvent, SensorEvent @@ -8,21 +12,36 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, SensorEntity, + SensorEntityDescription, ) from homeassistant.const import ( CONF_DEVICES, + DEGREE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_VOLTAGE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + LENGTH_MILLIMETERS, + PERCENTAGE, + POWER_WATT, + PRECIPITATION_MILLIMETERS_PER_HOUR, + PRESSURE_HPA, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + SPEED_METERS_PER_SECOND, + TEMP_CELSIUS, + UV_INDEX, ) from homeassistant.core import callback +from homeassistant.util import dt from . import ( CONF_DATA_BITS, - DATA_TYPES, RfxtrxEntity, connect_auto_add, get_device_id, @@ -47,25 +66,161 @@ def _rssi_convert(value): return f"{value*8-120}" -DEVICE_CLASSES = { - "Barometer": DEVICE_CLASS_PRESSURE, - "Battery numeric": DEVICE_CLASS_BATTERY, - "Current Ch. 1": DEVICE_CLASS_CURRENT, - "Current Ch. 2": DEVICE_CLASS_CURRENT, - "Current Ch. 3": DEVICE_CLASS_CURRENT, - "Energy usage": DEVICE_CLASS_POWER, - "Humidity": DEVICE_CLASS_HUMIDITY, - "Rssi numeric": DEVICE_CLASS_SIGNAL_STRENGTH, - "Temperature": DEVICE_CLASS_TEMPERATURE, - "Total usage": DEVICE_CLASS_ENERGY, - "Voltage": DEVICE_CLASS_VOLTAGE, -} +@dataclass +class RfxtrxSensorEntityDescription(SensorEntityDescription): + """Description of sensor entities.""" + + convert: Callable = lambda x: x -CONVERT_FUNCTIONS = { - "Battery numeric": _battery_convert, - "Rssi numeric": _rssi_convert, -} +SENSOR_TYPES = ( + RfxtrxSensorEntityDescription( + key="Barameter", + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=PRESSURE_HPA, + ), + RfxtrxSensorEntityDescription( + key="Battery numeric", + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=PERCENTAGE, + convert=_battery_convert, + ), + RfxtrxSensorEntityDescription( + key="Current", + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + ), + RfxtrxSensorEntityDescription( + key="Current Ch. 1", + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + ), + RfxtrxSensorEntityDescription( + key="Current Ch. 2", + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + ), + RfxtrxSensorEntityDescription( + key="Current Ch. 3", + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + ), + RfxtrxSensorEntityDescription( + key="Energy usage", + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=POWER_WATT, + ), + RfxtrxSensorEntityDescription( + key="Humidity", + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=PERCENTAGE, + ), + RfxtrxSensorEntityDescription( + key="Rssi numeric", + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + convert=_rssi_convert, + ), + RfxtrxSensorEntityDescription( + key="Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=TEMP_CELSIUS, + ), + RfxtrxSensorEntityDescription( + key="Temperature2", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=TEMP_CELSIUS, + ), + RfxtrxSensorEntityDescription( + key="Total usage", + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt.utc_from_timestamp(0), + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + RfxtrxSensorEntityDescription( + key="Voltage", + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + ), + RfxtrxSensorEntityDescription( + key="Wind direction", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=DEGREE, + ), + RfxtrxSensorEntityDescription( + key="Rain rate", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, + ), + RfxtrxSensorEntityDescription( + key="Sound", + ), + RfxtrxSensorEntityDescription( + key="Sensor Status", + ), + RfxtrxSensorEntityDescription( + key="Count", + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt.utc_from_timestamp(0), + unit_of_measurement="count", + ), + RfxtrxSensorEntityDescription( + key="Counter value", + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt.utc_from_timestamp(0), + unit_of_measurement="count", + ), + RfxtrxSensorEntityDescription( + key="Chill", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=TEMP_CELSIUS, + ), + RfxtrxSensorEntityDescription( + key="Wind average speed", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=SPEED_METERS_PER_SECOND, + ), + RfxtrxSensorEntityDescription( + key="Wind gust", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=SPEED_METERS_PER_SECOND, + ), + RfxtrxSensorEntityDescription( + key="Rain total", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=LENGTH_MILLIMETERS, + ), + RfxtrxSensorEntityDescription( + key="Forecast", + ), + RfxtrxSensorEntityDescription( + key="Forecast numeric", + ), + RfxtrxSensorEntityDescription( + key="Humidity status", + ), + RfxtrxSensorEntityDescription( + key="UV", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=UV_INDEX, + ), +) + +SENSOR_TYPES_DICT = {desc.key: desc for desc in SENSOR_TYPES} async def async_setup_entry( @@ -92,13 +247,13 @@ async def async_setup_entry( device_id = get_device_id( event.device, data_bits=entity_info.get(CONF_DATA_BITS) ) - for data_type in set(event.values) & set(DATA_TYPES): + 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) - entity = RfxtrxSensor(event.device, device_id, data_type) + entity = RfxtrxSensor(event.device, device_id, SENSOR_TYPES_DICT[data_type]) entities.append(entity) async_add_entities(entities) @@ -109,7 +264,7 @@ async def async_setup_entry( if not supported(event): return - for data_type in set(event.values) & set(DATA_TYPES): + for data_type in set(event.values) & set(SENSOR_TYPES_DICT): data_id = (*device_id, data_type) if data_id in data_ids: continue @@ -123,7 +278,9 @@ async def async_setup_entry( "".join(f"{x:02x}" for x in event.data), ) - entity = RfxtrxSensor(event.device, device_id, data_type, event=event) + entity = RfxtrxSensor( + event.device, device_id, SENSOR_TYPES_DICT[data_type], event=event + ) async_add_entities([entity]) # Subscribe to main RFXtrx events @@ -133,16 +290,16 @@ async def async_setup_entry( class RfxtrxSensor(RfxtrxEntity, SensorEntity): """Representation of a RFXtrx sensor.""" - def __init__(self, device, device_id, data_type, event=None): + entity_description: RfxtrxSensorEntityDescription + + def __init__(self, device, device_id, entity_description, event=None): """Initialize the sensor.""" super().__init__(device, device_id, event=event) - self.data_type = data_type - self._unit_of_measurement = DATA_TYPES.get(data_type) - self._name = f"{device.type_string} {device.id_string} {data_type}" - self._unique_id = "_".join(x for x in (*self._device_id, data_type)) - - self._device_class = DEVICE_CLASSES.get(data_type) - self._convert_fun = CONVERT_FUNCTIONS.get(data_type, lambda x: x) + self.entity_description = entity_description + self._name = f"{device.type_string} {device.id_string} {entity_description.key}" + self._unique_id = "_".join( + x for x in (*self._device_id, entity_description.key) + ) async def async_added_to_hass(self): """Restore device state.""" @@ -160,13 +317,8 @@ class RfxtrxSensor(RfxtrxEntity, SensorEntity): """Return the state of the sensor.""" if not self._event: return None - value = self._event.values.get(self.data_type) - return self._convert_fun(value) - - @property - def unit_of_measurement(self): - """Return the unit this state is expressed in.""" - return self._unit_of_measurement + value = self._event.values.get(self.entity_description.key) + return self.entity_description.convert(value) @property def should_poll(self): @@ -178,18 +330,13 @@ class RfxtrxSensor(RfxtrxEntity, SensorEntity): """We should force updates. Repeated states have meaning.""" return True - @property - def device_class(self): - """Return a device class for sensor.""" - return self._device_class - @callback def _handle_event(self, event, device_id): """Check if event applies to me and update.""" if device_id != self._device_id: return - if self.data_type not in event.values: + if self.entity_description.key not in event.values: return _LOGGER.debug( From acc0288f4cbf82868d3fcd51a1a0581f598c4ae3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Aug 2021 14:48:00 -0500 Subject: [PATCH 018/355] Bump zeroconf to 0.33.4 to ensure zeroconf can startup when ipv6 is disabled (#54165) Changelog: https://github.com/jstasiak/python-zeroconf/compare/0.33.3...0.33.4 --- 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 7b3cfa1fefd..1847a1c806b 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.33.3"], + "requirements": ["zeroconf==0.33.4"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d435f165f61..617a057743b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ sqlalchemy==1.4.17 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 -zeroconf==0.33.3 +zeroconf==0.33.4 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 3aec770dc6b..5dc4d86d603 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2442,7 +2442,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.33.3 +zeroconf==0.33.4 # homeassistant.components.zha zha-quirks==0.0.59 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10794fe2c94..83dfc9695de 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1347,7 +1347,7 @@ youless-api==0.10 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.33.3 +zeroconf==0.33.4 # homeassistant.components.zha zha-quirks==0.0.59 From 099a1de92b7b19f8b550300e437aac53d410d1c4 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sat, 7 Aug 2021 00:43:39 +0200 Subject: [PATCH 019/355] Use SensorEntityDescription for AsusWRT sensors (#54111) --- homeassistant/components/asuswrt/sensor.py | 175 ++++++++++++--------- 1 file changed, 100 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 679ae832394..6c0671b53cb 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -1,11 +1,16 @@ """Asuswrt status sensors.""" from __future__ import annotations +from dataclasses import dataclass import logging from numbers import Number from typing import Any -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND from homeassistant.core import HomeAssistant @@ -14,6 +19,7 @@ from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) +from homeassistant.util import dt as dt_util from .const import ( DATA_ASUSWRT, @@ -25,62 +31,83 @@ from .const import ( ) from .router import KEY_COORDINATOR, KEY_SENSORS, AsusWrtRouter + +@dataclass +class AsusWrtSensorEntityDescription(SensorEntityDescription): + """A class that describes AsusWrt sensor entities.""" + + factor: int | None = None + precision: int = 2 + + DEFAULT_PREFIX = "Asuswrt" - -SENSOR_DEVICE_CLASS = "device_class" -SENSOR_ICON = "icon" -SENSOR_NAME = "name" -SENSOR_UNIT = "unit" -SENSOR_FACTOR = "factor" -SENSOR_DEFAULT_ENABLED = "default_enabled" - UNIT_DEVICES = "Devices" -CONNECTION_SENSORS = { - SENSORS_CONNECTED_DEVICE[0]: { - SENSOR_NAME: "Devices Connected", - SENSOR_UNIT: UNIT_DEVICES, - SENSOR_FACTOR: 0, - SENSOR_ICON: "mdi:router-network", - SENSOR_DEFAULT_ENABLED: True, - }, - SENSORS_RATES[0]: { - SENSOR_NAME: "Download Speed", - SENSOR_UNIT: DATA_RATE_MEGABITS_PER_SECOND, - SENSOR_FACTOR: 125000, - SENSOR_ICON: "mdi:download-network", - }, - SENSORS_RATES[1]: { - SENSOR_NAME: "Upload Speed", - SENSOR_UNIT: DATA_RATE_MEGABITS_PER_SECOND, - SENSOR_FACTOR: 125000, - SENSOR_ICON: "mdi:upload-network", - }, - SENSORS_BYTES[0]: { - SENSOR_NAME: "Download", - SENSOR_UNIT: DATA_GIGABYTES, - SENSOR_FACTOR: 1000000000, - SENSOR_ICON: "mdi:download", - }, - SENSORS_BYTES[1]: { - SENSOR_NAME: "Upload", - SENSOR_UNIT: DATA_GIGABYTES, - SENSOR_FACTOR: 1000000000, - SENSOR_ICON: "mdi:upload", - }, - SENSORS_LOAD_AVG[0]: { - SENSOR_NAME: "Load Avg (1m)", - SENSOR_ICON: "mdi:cpu-32-bit", - }, - SENSORS_LOAD_AVG[1]: { - SENSOR_NAME: "Load Avg (5m)", - SENSOR_ICON: "mdi:cpu-32-bit", - }, - SENSORS_LOAD_AVG[2]: { - SENSOR_NAME: "Load Avg (15m)", - SENSOR_ICON: "mdi:cpu-32-bit", - }, -} +CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( + AsusWrtSensorEntityDescription( + key=SENSORS_CONNECTED_DEVICE[0], + name="Devices Connected", + icon="mdi:router-network", + unit_of_measurement=UNIT_DEVICES, + entity_registry_enabled_default=True, + ), + AsusWrtSensorEntityDescription( + key=SENSORS_RATES[0], + name="Download Speed", + icon="mdi:download-network", + unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, + entity_registry_enabled_default=False, + factor=125000, + ), + AsusWrtSensorEntityDescription( + key=SENSORS_RATES[1], + name="Upload Speed", + icon="mdi:upload-network", + unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, + entity_registry_enabled_default=False, + factor=125000, + ), + AsusWrtSensorEntityDescription( + key=SENSORS_BYTES[0], + name="Download", + icon="mdi:download", + unit_of_measurement=DATA_GIGABYTES, + entity_registry_enabled_default=False, + factor=1000000000, + ), + AsusWrtSensorEntityDescription( + key=SENSORS_BYTES[1], + name="Upload", + icon="mdi:upload", + unit_of_measurement=DATA_GIGABYTES, + entity_registry_enabled_default=False, + factor=1000000000, + ), + AsusWrtSensorEntityDescription( + key=SENSORS_LOAD_AVG[0], + name="Load Avg (1m)", + icon="mdi:cpu-32-bit", + entity_registry_enabled_default=False, + factor=1, + precision=1, + ), + AsusWrtSensorEntityDescription( + key=SENSORS_LOAD_AVG[1], + name="Load Avg (5m)", + icon="mdi:cpu-32-bit", + entity_registry_enabled_default=False, + factor=1, + precision=1, + ), + AsusWrtSensorEntityDescription( + key=SENSORS_LOAD_AVG[2], + name="Load Avg (15m)", + icon="mdi:cpu-32-bit", + entity_registry_enabled_default=False, + factor=1, + precision=1, + ), +) _LOGGER = logging.getLogger(__name__) @@ -95,13 +122,13 @@ async def async_setup_entry( for sensor_data in router.sensors_coordinator.values(): coordinator = sensor_data[KEY_COORDINATOR] sensors = sensor_data[KEY_SENSORS] - for sensor_key in sensors: - if sensor_key in CONNECTION_SENSORS: - entities.append( - AsusWrtSensor( - coordinator, router, sensor_key, CONNECTION_SENSORS[sensor_key] - ) - ) + entities.extend( + [ + AsusWrtSensor(coordinator, router, sensor_descr) + for sensor_descr in CONNECTION_SENSORS + if sensor_descr.key in sensors + ] + ) async_add_entities(entities, True) @@ -113,31 +140,29 @@ class AsusWrtSensor(CoordinatorEntity, SensorEntity): self, coordinator: DataUpdateCoordinator, router: AsusWrtRouter, - sensor_type: str, - sensor_def: dict[str, Any], + description: AsusWrtSensorEntityDescription, ) -> None: """Initialize a AsusWrt sensor.""" super().__init__(coordinator) self._router = router - self._sensor_type = sensor_type - self._attr_name = f"{DEFAULT_PREFIX} {sensor_def[SENSOR_NAME]}" - self._factor = sensor_def.get(SENSOR_FACTOR) + self.entity_description = description + + self._attr_name = f"{DEFAULT_PREFIX} {description.name}" self._attr_unique_id = f"{DOMAIN} {self.name}" - self._attr_entity_registry_enabled_default = sensor_def.get( - SENSOR_DEFAULT_ENABLED, False - ) - self._attr_unit_of_measurement = sensor_def.get(SENSOR_UNIT) - self._attr_icon = sensor_def.get(SENSOR_ICON) - self._attr_device_class = sensor_def.get(SENSOR_DEVICE_CLASS) + self._attr_state_class = STATE_CLASS_MEASUREMENT + + if description.unit_of_measurement == DATA_GIGABYTES: + self._attr_last_reset = dt_util.utc_from_timestamp(0) @property def state(self) -> str: """Return current state.""" - state = self.coordinator.data.get(self._sensor_type) + descr = self.entity_description + state = self.coordinator.data.get(descr.key) if state is None: return None - if self._factor and isinstance(state, Number): - return round(state / self._factor, 2) + if descr.factor and isinstance(state, Number): + return round(state / descr.factor, descr.precision) return state @property From 98bcdc2cf5536c0f88f27fb02296078a3f878791 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 7 Aug 2021 00:10:12 +0000 Subject: [PATCH 020/355] [ci skip] Translation update --- .../accuweather/translations/hu.json | 3 +- .../components/adax/translations/no.json | 20 +++++++++++ .../airvisual/translations/sensor.no.json | 14 +++++++- .../alarm_control_panel/translations/no.json | 4 +++ .../components/arcam_fmj/translations/hu.json | 5 +++ .../components/co2signal/translations/no.json | 34 +++++++++++++++++++ .../components/coinbase/translations/no.json | 1 + .../components/energy/translations/no.json | 3 ++ .../components/enocean/translations/hu.json | 6 ++-- .../components/flipr/translations/no.json | 30 ++++++++++++++++ .../flunearyou/translations/hu.json | 3 +- .../forecast_solar/translations/no.json | 2 +- .../forked_daapd/translations/hu.json | 20 +++++++++-- .../growatt_server/translations/no.json | 1 + .../homeassistant/translations/no.json | 1 + .../components/homekit/translations/no.json | 2 +- .../components/honeywell/translations/no.json | 17 ++++++++++ .../huawei_lte/translations/no.json | 5 +-- .../components/hue/translations/hu.json | 3 +- .../components/light/translations/hu.json | 1 + .../components/litejet/translations/no.json | 10 ++++++ .../motion_blinds/translations/hu.json | 11 ++++-- .../nfandroidtv/translations/no.json | 21 ++++++++++++ .../nmap_tracker/translations/no.json | 4 ++- .../components/powerwall/translations/hu.json | 1 + .../progettihwsw/translations/hu.json | 3 +- .../components/prosegur/translations/no.json | 11 ++++++ .../components/renault/translations/no.json | 17 +++++++++- .../components/rfxtrx/translations/hu.json | 3 +- .../components/sentry/translations/hu.json | 2 ++ .../simplisafe/translations/hu.json | 2 ++ .../simplisafe/translations/no.json | 2 +- .../somfy_mylink/translations/hu.json | 1 + .../components/sonos/translations/no.json | 1 + .../switcher_kis/translations/no.json | 13 +++++++ .../components/syncthru/translations/hu.json | 1 + .../synology_dsm/translations/no.json | 11 +++++- .../components/tesla/translations/no.json | 1 + .../components/toon/translations/hu.json | 3 ++ .../components/tractive/translations/de.json | 19 +++++++++++ .../components/tractive/translations/no.json | 19 +++++++++++ .../uptimerobot/translations/de.json | 4 ++- .../uptimerobot/translations/no.json | 20 +++++++++++ .../wolflink/translations/sensor.hu.json | 4 +++ .../xiaomi_miio/translations/select.no.json | 9 +++++ .../yale_smart_alarm/translations/no.json | 8 +++++ .../components/youless/translations/no.json | 4 +++ .../components/zha/translations/nn.json | 3 ++ .../components/zwave_js/translations/no.json | 15 ++++++++ 49 files changed, 376 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/adax/translations/no.json create mode 100644 homeassistant/components/co2signal/translations/no.json create mode 100644 homeassistant/components/energy/translations/no.json create mode 100644 homeassistant/components/flipr/translations/no.json create mode 100644 homeassistant/components/honeywell/translations/no.json create mode 100644 homeassistant/components/nfandroidtv/translations/no.json create mode 100644 homeassistant/components/switcher_kis/translations/no.json create mode 100644 homeassistant/components/tractive/translations/de.json create mode 100644 homeassistant/components/tractive/translations/no.json create mode 100644 homeassistant/components/uptimerobot/translations/no.json create mode 100644 homeassistant/components/xiaomi_miio/translations/select.no.json diff --git a/homeassistant/components/accuweather/translations/hu.json b/homeassistant/components/accuweather/translations/hu.json index 3cb78005d46..7b4d270f78b 100644 --- a/homeassistant/components/accuweather/translations/hu.json +++ b/homeassistant/components/accuweather/translations/hu.json @@ -5,7 +5,8 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs" + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", + "requests_exceeded": "T\u00fall\u00e9pt\u00e9k az Accuweather API-hoz beny\u00fajtott k\u00e9relmek megengedett sz\u00e1m\u00e1t. Meg kell v\u00e1rnia vagy m\u00f3dos\u00edtania kell az API-kulcsot." }, "step": { "user": { diff --git a/homeassistant/components/adax/translations/no.json b/homeassistant/components/adax/translations/no.json new file mode 100644 index 00000000000..33c54b57093 --- /dev/null +++ b/homeassistant/components/adax/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" + }, + "step": { + "user": { + "data": { + "account_id": "Konto-ID", + "host": "Vert", + "password": "Passord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.no.json b/homeassistant/components/airvisual/translations/sensor.no.json index 86c95f8e8f2..cf142ad9f1a 100644 --- a/homeassistant/components/airvisual/translations/sensor.no.json +++ b/homeassistant/components/airvisual/translations/sensor.no.json @@ -1,8 +1,20 @@ { "state": { "airvisual__pollutant_label": { + "co": "Karbonmonoksid", + "n2": "Nitrogendioksid", + "o3": "Ozon", "p1": "PM10", - "p2": "PM2.5" + "p2": "PM2.5", + "s2": "Svoveldioksid" + }, + "airvisual__pollutant_level": { + "good": "Bra", + "hazardous": "Farlig", + "moderate": "Moderat", + "unhealthy": "Usunt", + "unhealthy_sensitive": "Usunt for sensitive grupper", + "very_unhealthy": "Veldig usunt" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/translations/no.json b/homeassistant/components/alarm_control_panel/translations/no.json index 465dd250086..ad8ed2c9c74 100644 --- a/homeassistant/components/alarm_control_panel/translations/no.json +++ b/homeassistant/components/alarm_control_panel/translations/no.json @@ -4,6 +4,7 @@ "arm_away": "Aktiver {entity_name} borte", "arm_home": "Aktiver {entity_name} hjemme", "arm_night": "Aktiver {entity_name} natt", + "arm_vacation": "{entity_name} ferie", "disarm": "Deaktiver {entity_name}", "trigger": "Utl\u00f8ser {entity_name}" }, @@ -11,6 +12,7 @@ "is_armed_away": "{entity_name} er aktivert borte", "is_armed_home": "{entity_name} er aktivert hjemme", "is_armed_night": "{entity_name} er aktivert natt", + "is_armed_vacation": "{entity_name} er armert ferie", "is_disarmed": "{entity_name} er deaktivert", "is_triggered": "{entity_name} er utl\u00f8st" }, @@ -18,6 +20,7 @@ "armed_away": "{entity_name} aktivert borte", "armed_home": "{entity_name} aktivert hjemme", "armed_night": "{entity_name} aktivert natt", + "armed_vacation": "{entity_name} armert ferie", "disarmed": "{entity_name} deaktivert", "triggered": "{entity_name} utl\u00f8st" } @@ -29,6 +32,7 @@ "armed_custom_bypass": "Armert tilpasset unntak", "armed_home": "Armert hjemme", "armed_night": "Armert natt", + "armed_vacation": "Armert ferie", "arming": "Armerer", "disarmed": "Avsl\u00e5tt", "disarming": "Disarmer", diff --git a/homeassistant/components/arcam_fmj/translations/hu.json b/homeassistant/components/arcam_fmj/translations/hu.json index dfccbbe7143..9539ad39bed 100644 --- a/homeassistant/components/arcam_fmj/translations/hu.json +++ b/homeassistant/components/arcam_fmj/translations/hu.json @@ -22,5 +22,10 @@ "description": "K\u00e9rj\u00fck, adja meg az eszk\u00f6z gazdag\u00e9pnev\u00e9t vagy IP-c\u00edm\u00e9t." } } + }, + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} bekapcsol\u00e1s\u00e1t k\u00e9rt\u00e9k" + } } } \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/no.json b/homeassistant/components/co2signal/translations/no.json new file mode 100644 index 00000000000..bb56f0c1364 --- /dev/null +++ b/homeassistant/components/co2signal/translations/no.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "api_ratelimit": "API Ratelimit overskredet", + "unknown": "Uventet feil" + }, + "error": { + "api_ratelimit": "API Ratelimit overskredet", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Breddegrad", + "longitude": "Lengdegrad" + } + }, + "country": { + "data": { + "country_code": "Landskode" + } + }, + "user": { + "data": { + "api_key": "Tilgangstoken", + "location": "Hent data for" + }, + "description": "Bes\u00f8k https://co2signal.com/ for \u00e5 be om et token." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/no.json b/homeassistant/components/coinbase/translations/no.json index 747049fbd5c..78cf46d717a 100644 --- a/homeassistant/components/coinbase/translations/no.json +++ b/homeassistant/components/coinbase/translations/no.json @@ -31,6 +31,7 @@ "init": { "data": { "account_balance_currencies": "Lommeboksaldoer som skal rapporteres.", + "exchange_base": "Standardvaluta for valutakurssensorer.", "exchange_rate_currencies": "Valutakurser som skal rapporteres." }, "description": "Juster Coinbase-alternativer" diff --git a/homeassistant/components/energy/translations/no.json b/homeassistant/components/energy/translations/no.json new file mode 100644 index 00000000000..168ae4ae877 --- /dev/null +++ b/homeassistant/components/energy/translations/no.json @@ -0,0 +1,3 @@ +{ + "title": "Energi" +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/hu.json b/homeassistant/components/enocean/translations/hu.json index 9cc6843682c..bfb6cb0499d 100644 --- a/homeassistant/components/enocean/translations/hu.json +++ b/homeassistant/components/enocean/translations/hu.json @@ -11,12 +11,14 @@ "detect": { "data": { "path": "USB dongle el\u00e9r\u00e9si \u00fatja" - } + }, + "title": "V\u00e1lassza ki az ENOcean-dongle el\u00e9r\u00e9si \u00fatvonal\u00e1t." }, "manual": { "data": { "path": "USB dongle el\u00e9r\u00e9si \u00fatja" - } + }, + "title": "Adja meg az ENOcean dongle el\u00e9r\u00e9si \u00fatvonal\u00e1t" } } } diff --git a/homeassistant/components/flipr/translations/no.json b/homeassistant/components/flipr/translations/no.json new file mode 100644 index 00000000000..550b0bae058 --- /dev/null +++ b/homeassistant/components/flipr/translations/no.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "no_flipr_id_found": "Ingen flipr -ID er knyttet til kontoen din forel\u00f8pig. Du b\u00f8r bekrefte at den fungerer med Flipr -mobilappen f\u00f8rst.", + "unknown": "Uventet feil" + }, + "step": { + "flipr_id": { + "data": { + "flipr_id": "Flipr ID" + }, + "description": "Velg din Flipr -ID i listen", + "title": "Velg din Flipr" + }, + "user": { + "data": { + "email": "E-post", + "password": "Passord" + }, + "description": "Koble til ved hjelp av Flipr-kontoen din.", + "title": "Koble til Flipr" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/hu.json b/homeassistant/components/flunearyou/translations/hu.json index b9ef1712ced..a67bc91a2a1 100644 --- a/homeassistant/components/flunearyou/translations/hu.json +++ b/homeassistant/components/flunearyou/translations/hu.json @@ -12,7 +12,8 @@ "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g" }, - "description": "Figyelje a felhaszn\u00e1l\u00f3alap\u00fa \u00e9s a CDC jelent\u00e9seket egy p\u00e1r koordin\u00e1t\u00e1ra." + "description": "Figyelje a felhaszn\u00e1l\u00f3alap\u00fa \u00e9s a CDC jelent\u00e9seket egy p\u00e1r koordin\u00e1t\u00e1ra.", + "title": "Flu Near You weboldal konfigur\u00e1l\u00e1sa" } } } diff --git a/homeassistant/components/forecast_solar/translations/no.json b/homeassistant/components/forecast_solar/translations/no.json index 5ee0691ecda..1504727c1ae 100644 --- a/homeassistant/components/forecast_solar/translations/no.json +++ b/homeassistant/components/forecast_solar/translations/no.json @@ -24,7 +24,7 @@ "declination": "Deklinasjon (0 = horisontal, 90 = vertikal)", "modules power": "Total Watt-toppeffekt i solcellemodulene dine" }, - "description": "Disse verdiene tillater justering av Solar.Forecast-resultatet. Se dokumentasjonen er et felt som er uklart." + "description": "Disse verdiene tillater justering av Solar.Forecast -resultatet. Se dokumentasjonen hvis et felt er uklart." } } } diff --git a/homeassistant/components/forked_daapd/translations/hu.json b/homeassistant/components/forked_daapd/translations/hu.json index aac95b2956a..bbf8cb560ff 100644 --- a/homeassistant/components/forked_daapd/translations/hu.json +++ b/homeassistant/components/forked_daapd/translations/hu.json @@ -9,7 +9,8 @@ "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", "websocket_not_enabled": "forked-daapd szerver websocket nincs enged\u00e9lyezve.", "wrong_host_or_port": "Nem tud csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a gazdag\u00e9pet \u00e9s a portot.", - "wrong_password": "Helytelen jelsz\u00f3." + "wrong_password": "Helytelen jelsz\u00f3.", + "wrong_server_type": "A forked-daapd integr\u00e1ci\u00f3hoz forked-daapd szerver sz\u00fcks\u00e9ges, amelynek verzi\u00f3ja> = 27.0." }, "flow_title": "{name} ({host})", "step": { @@ -19,7 +20,22 @@ "name": "Megjelen\u00edt\u00e9si n\u00e9v", "password": "API jelsz\u00f3 (hagyja \u00fcresen, ha nincs jelsz\u00f3)", "port": "API port" - } + }, + "title": "\u00c1ll\u00edtsa be a forked-daapd eszk\u00f6zt" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "librespot_java_port": "Port librespot-java cs\u0151 vez\u00e9rl\u00e9s (ha van)", + "max_playlists": "Forr\u00e1sk\u00e9nt haszn\u00e1lt lej\u00e1tsz\u00e1si list\u00e1k maxim\u00e1lis sz\u00e1ma", + "tts_pause_time": "M\u00e1sodpercek a TTS el\u0151tti \u00e9s ut\u00e1ni sz\u00fcnethez", + "tts_volume": "TTS hanger\u0151 (lebeg\u0151 a [0,1] tartom\u00e1nyban)" + }, + "description": "A forked-daapd integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sai.", + "title": "A forked-daapd be\u00e1ll\u00edt\u00e1sainak konfigur\u00e1l\u00e1sa" } } } diff --git a/homeassistant/components/growatt_server/translations/no.json b/homeassistant/components/growatt_server/translations/no.json index dee1e989465..8977a7e86a3 100644 --- a/homeassistant/components/growatt_server/translations/no.json +++ b/homeassistant/components/growatt_server/translations/no.json @@ -17,6 +17,7 @@ "data": { "name": "Navn", "password": "Passord", + "url": "URL", "username": "Brukernavn" }, "title": "Skriv inn Growatt-informasjonen din" diff --git a/homeassistant/components/homeassistant/translations/no.json b/homeassistant/components/homeassistant/translations/no.json index 325bb53db15..675c02a6b66 100644 --- a/homeassistant/components/homeassistant/translations/no.json +++ b/homeassistant/components/homeassistant/translations/no.json @@ -10,6 +10,7 @@ "os_version": "Operativsystemversjon", "python_version": "Python versjon", "timezone": "Tidssone", + "user": "Bruker", "version": "Versjon", "virtualenv": "Virtuelt milj\u00f8" } diff --git a/homeassistant/components/homekit/translations/no.json b/homeassistant/components/homekit/translations/no.json index 7de5494c56a..2a4f1497e2f 100644 --- a/homeassistant/components/homekit/translations/no.json +++ b/homeassistant/components/homekit/translations/no.json @@ -12,7 +12,7 @@ "data": { "include_domains": "Domener \u00e5 inkludere" }, - "description": "Velg domenene som skal inkluderes. Alle st\u00f8ttede enheter i domenet vil bli inkludert. Det opprettes en egen HomeKit-forekomst i tilbeh\u00f8rsmodus for hver tv-mediaspiller og kamera.", + "description": "Velg domenene som skal inkluderes. Alle enheter som st\u00f8ttes p\u00e5 domenet vil bli inkludert. En egen HomeKit -forekomst i tilbeh\u00f8rsmodus vil bli opprettet for hver tv -mediespiller, aktivitetsbasert fjernkontroll, l\u00e5s og kamera.", "title": "Velg domener som skal inkluderes" } } diff --git a/homeassistant/components/honeywell/translations/no.json b/homeassistant/components/honeywell/translations/no.json new file mode 100644 index 00000000000..97d31d34961 --- /dev/null +++ b/homeassistant/components/honeywell/translations/no.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_auth": "Ugyldig godkjenning" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Vennligst skriv inn legitimasjonen som brukes for \u00e5 logge deg p\u00e5 mytotalconnectcomfort.com.", + "title": "Honeywell Total Connect Comfort (USA)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/no.json b/homeassistant/components/huawei_lte/translations/no.json index a328858c57f..3c8b26ab0cd 100644 --- a/homeassistant/components/huawei_lte/translations/no.json +++ b/homeassistant/components/huawei_lte/translations/no.json @@ -23,7 +23,7 @@ "url": "URL", "username": "Brukernavn" }, - "description": "Fyll inn detaljer for enhetstilgang. Spesifisering av brukernavn og passord er valgfritt, men gir st\u00f8tte for flere integrasjonsfunksjoner. P\u00e5 en annen side kan bruk av en autorisert tilkobling f\u00f8re til problemer med tilgang til enhetens webgrensesnitt utenfor Home Assistant mens integrasjonen er aktiv, og omvendt.", + "description": "Angi enhetsadgangsdetaljer.", "title": "Konfigurer Huawei LTE" } } @@ -35,7 +35,8 @@ "name": "Navn p\u00e5 varslingstjeneste (endring krever omstart)", "recipient": "Mottakere av SMS-varsling", "track_new_devices": "Spor nye enheter", - "track_wired_clients": "Spor kablede nettverksklienter" + "track_wired_clients": "Spor kablede nettverksklienter", + "unauthenticated_mode": "Uautentisert modus (endring krever omlasting)" } } } diff --git a/homeassistant/components/hue/translations/hu.json b/homeassistant/components/hue/translations/hu.json index 91321f9c6fd..30084ee9940 100644 --- a/homeassistant/components/hue/translations/hu.json +++ b/homeassistant/components/hue/translations/hu.json @@ -58,7 +58,8 @@ "step": { "init": { "data": { - "allow_hue_groups": "Hue csoportok enged\u00e9lyez\u00e9se" + "allow_hue_groups": "Hue csoportok enged\u00e9lyez\u00e9se", + "allow_unreachable": "Hagyja, hogy az el\u00e9rhetetlen izz\u00f3k helyesen jelents\u00e9k \u00e1llapotukat" } } } diff --git a/homeassistant/components/light/translations/hu.json b/homeassistant/components/light/translations/hu.json index ad215a5ba4c..1ac835fd1af 100644 --- a/homeassistant/components/light/translations/hu.json +++ b/homeassistant/components/light/translations/hu.json @@ -3,6 +3,7 @@ "action_type": { "brightness_decrease": "{entity_name} f\u00e9nyerej\u00e9nek cs\u00f6kkent\u00e9se", "brightness_increase": "{entity_name} f\u00e9nyerej\u00e9nek n\u00f6vel\u00e9se", + "flash": "Vaku {entity_name}", "toggle": "{entity_name} fel/lekapcsol\u00e1sa", "turn_off": "{entity_name} lekapcsol\u00e1sa", "turn_on": "{entity_name} felkapcsol\u00e1sa" diff --git a/homeassistant/components/litejet/translations/no.json b/homeassistant/components/litejet/translations/no.json index d3206ca2897..f6e2379900d 100644 --- a/homeassistant/components/litejet/translations/no.json +++ b/homeassistant/components/litejet/translations/no.json @@ -15,5 +15,15 @@ "title": "Koble til LiteJet" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "Standard overgang (sekunder)" + }, + "title": "Konfigurer LiteJet" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/hu.json b/homeassistant/components/motion_blinds/translations/hu.json index 19f0c70c4d6..a2560e5fa79 100644 --- a/homeassistant/components/motion_blinds/translations/hu.json +++ b/homeassistant/components/motion_blinds/translations/hu.json @@ -8,24 +8,29 @@ "error": { "discovery_error": "Nem siker\u00fclt felfedezni a Motion Gateway-t" }, + "flow_title": "Mozg\u00f3 red\u0151ny", "step": { "connect": { "data": { "api_key": "API kulcs" }, - "description": "Sz\u00fcks\u00e9ge lesz a 16 karakteres API kulcsra, \u00fatmutat\u00e1s\u00e9rt l\u00e1sd: https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key" + "description": "Sz\u00fcks\u00e9ge lesz a 16 karakteres API kulcsra, \u00fatmutat\u00e1s\u00e9rt l\u00e1sd: https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", + "title": "Mozg\u00f3 red\u0151ny" }, "select": { "data": { "select_ip": "IP c\u00edm" - } + }, + "description": "Futtassa \u00fajra a be\u00e1ll\u00edt\u00e1st, ha tov\u00e1bbi Motion Gateway-eket szeretne csatlakoztatni", + "title": "V\u00e1lassza ki a csatlakoztatni k\u00edv\u00e1nt Motion Gateway-t" }, "user": { "data": { "api_key": "API kulcs", "host": "IP c\u00edm" }, - "description": "Csatlakozzon a Motion Gateway-hez, ha az IP-c\u00edm nincs be\u00e1ll\u00edtva, akkor az automatikus felder\u00edt\u00e9st haszn\u00e1lja" + "description": "Csatlakozzon a Motion Gateway-hez, ha az IP-c\u00edm nincs be\u00e1ll\u00edtva, akkor az automatikus felder\u00edt\u00e9st haszn\u00e1lja", + "title": "Mozg\u00f3 red\u0151ny" } } } diff --git a/homeassistant/components/nfandroidtv/translations/no.json b/homeassistant/components/nfandroidtv/translations/no.json new file mode 100644 index 00000000000..e8aea574c96 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "name": "Navn" + }, + "description": "Denne integrasjonen krever Notifications for Android TV -appen. \n\n For Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\n For Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\n Du b\u00f8r konfigurere enten DHCP -reservasjon p\u00e5 ruteren din (se brukerh\u00e5ndboken til ruteren din) eller en statisk IP -adresse p\u00e5 enheten. Hvis ikke, vil enheten til slutt bli utilgjengelig.", + "title": "Varsler for Android TV / Fire TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/no.json b/homeassistant/components/nmap_tracker/translations/no.json index 487d15c910f..03a241bc3a2 100644 --- a/homeassistant/components/nmap_tracker/translations/no.json +++ b/homeassistant/components/nmap_tracker/translations/no.json @@ -28,7 +28,9 @@ "exclude": "Nettverksadresser (kommaseparert) for \u00e5 ekskludere fra skanning", "home_interval": "Minimum antall minutter mellom skanninger av aktive enheter (lagre batteri)", "hosts": "Nettverksadresser (kommaseparert) for \u00e5 skanne", - "scan_options": "R\u00e5 konfigurerbare skannealternativer for Nmap" + "interval_seconds": "Skanneintervall", + "scan_options": "R\u00e5 konfigurerbare skannealternativer for Nmap", + "track_new_devices": "Spor nye enheter" }, "description": "Konfigurer verter som skal skannes av Nmap. Nettverksadresse og ekskluderer kan v\u00e6re IP-adresser (192.168.1.1), IP-nettverk (192.168.0.0/24) eller IP-omr\u00e5der (192.168.1.0-32)." } diff --git a/homeassistant/components/powerwall/translations/hu.json b/homeassistant/components/powerwall/translations/hu.json index 1102ba78673..d5bc30e7d11 100644 --- a/homeassistant/components/powerwall/translations/hu.json +++ b/homeassistant/components/powerwall/translations/hu.json @@ -17,6 +17,7 @@ "ip_address": "IP c\u00edm", "password": "Jelsz\u00f3" }, + "description": "A jelsz\u00f3 \u00e1ltal\u00e1ban a Biztons\u00e1gi ment\u00e9s k\u00f6zponti egys\u00e9g sorozatsz\u00e1m\u00e1nak utols\u00f3 5 karaktere, \u00e9s megtal\u00e1lhat\u00f3 a Tesla alkalmaz\u00e1sban, vagy a jelsz\u00f3 utols\u00f3 5 karaktere a Biztons\u00e1gi ment\u00e9s k\u00f6zponti egys\u00e9g 2 ajtaj\u00e1ban.", "title": "Csatlakoz\u00e1s a powerwallhoz" } } diff --git a/homeassistant/components/progettihwsw/translations/hu.json b/homeassistant/components/progettihwsw/translations/hu.json index 76af6fb124f..fea70ec88ac 100644 --- a/homeassistant/components/progettihwsw/translations/hu.json +++ b/homeassistant/components/progettihwsw/translations/hu.json @@ -33,7 +33,8 @@ "data": { "host": "Hoszt", "port": "Port" - } + }, + "title": "\u00c1ll\u00edtsa be" } } } diff --git a/homeassistant/components/prosegur/translations/no.json b/homeassistant/components/prosegur/translations/no.json index 5732bb920b2..73bacd26c14 100644 --- a/homeassistant/components/prosegur/translations/no.json +++ b/homeassistant/components/prosegur/translations/no.json @@ -1,14 +1,25 @@ { "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, "step": { "reauth_confirm": { "data": { + "description": "Autentiser p\u00e5 nytt med Prosegur-kontoen.", "password": "Passord", "username": "Brukernavn" } }, "user": { "data": { + "country": "Land", "password": "Passord", "username": "Brukernavn" } diff --git a/homeassistant/components/renault/translations/no.json b/homeassistant/components/renault/translations/no.json index f367c8c540d..4675f939fdd 100644 --- a/homeassistant/components/renault/translations/no.json +++ b/homeassistant/components/renault/translations/no.json @@ -1,11 +1,26 @@ { "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "kamereon_no_account": "Kan ikke finne Kamereon -kontoen." + }, + "error": { + "invalid_credentials": "Ugyldig godkjenning" + }, "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereon -konto -ID" + }, + "title": "Velg Kamereon -konto -ID" + }, "user": { "data": { + "locale": "Lokal", "password": "Passord", "username": "E-Post" - } + }, + "title": "Angi Renault-legitimasjon" } } } diff --git a/homeassistant/components/rfxtrx/translations/hu.json b/homeassistant/components/rfxtrx/translations/hu.json index d8a27a3173b..5b953c1260e 100644 --- a/homeassistant/components/rfxtrx/translations/hu.json +++ b/homeassistant/components/rfxtrx/translations/hu.json @@ -69,7 +69,8 @@ "off_delay": "Kikapcsol\u00e1si k\u00e9sleltet\u00e9s", "off_delay_enabled": "Kikapcsol\u00e1si k\u00e9sleltet\u00e9s enged\u00e9lyez\u00e9se", "replace_device": "V\u00e1lassza ki a cser\u00e9lni k\u00edv\u00e1nt eszk\u00f6zt", - "signal_repetitions": "A jelism\u00e9tl\u00e9sek sz\u00e1ma" + "signal_repetitions": "A jelism\u00e9tl\u00e9sek sz\u00e1ma", + "venetian_blind_mode": "Velencei red\u0151ny \u00fczemm\u00f3d" }, "title": "Konfigur\u00e1lja az eszk\u00f6z be\u00e1ll\u00edt\u00e1sait" } diff --git a/homeassistant/components/sentry/translations/hu.json b/homeassistant/components/sentry/translations/hu.json index 43404f72495..df07c41449e 100644 --- a/homeassistant/components/sentry/translations/hu.json +++ b/homeassistant/components/sentry/translations/hu.json @@ -25,6 +25,8 @@ "event_custom_components": "Esem\u00e9nyek k\u00fcld\u00e9se egy\u00e9ni \u00f6sszetev\u0151kb\u0151l", "event_handled": "K\u00fcldj\u00f6n kezelt esem\u00e9nyeket", "event_third_party_packages": "K\u00fcldj\u00f6n esem\u00e9nyeket harmadik f\u00e9l csomagjaib\u00f3l", + "logging_event_level": "A napl\u00f3szint\u0171 Sentry esem\u00e9ny regisztr\u00e1l\u00e1sa", + "logging_level": "A napl\u00f3szint\u0171 Sentry a napl\u00f3k t\u00f6red\u00e9keinek r\u00f6gz\u00edt\u00e9se", "tracing": "Enged\u00e9lyezze a teljes\u00edtm\u00e9nyk\u00f6vet\u00e9st", "tracing_sample_rate": "A mintav\u00e9teli sebess\u00e9g nyomon k\u00f6vet\u00e9se; 0,0 \u00e9s 1,0 k\u00f6z\u00f6tt (1,0 = 100%)" } diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index 14faee90ed4..f7c1b5afd9d 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -7,10 +7,12 @@ "error": { "identifier_exists": "Fi\u00f3k m\u00e1r regisztr\u00e1lva van", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "still_awaiting_mfa": "M\u00e9g v\u00e1r az MFA e-mail kattint\u00e1sra", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { "mfa": { + "description": "Ellen\u0151rizze e-mailj\u00e9ben a SimpliSafe linkj\u00e9t. A link ellen\u0151rz\u00e9se ut\u00e1n t\u00e9rjen vissza ide, \u00e9s fejezze be az integr\u00e1ci\u00f3 telep\u00edt\u00e9s\u00e9t.", "title": "SimpliSafe t\u00f6bbt\u00e9nyez\u0151s hiteles\u00edt\u00e9s" }, "reauth_confirm": { diff --git a/homeassistant/components/simplisafe/translations/no.json b/homeassistant/components/simplisafe/translations/no.json index bc82715ad63..acd8adf0792 100644 --- a/homeassistant/components/simplisafe/translations/no.json +++ b/homeassistant/components/simplisafe/translations/no.json @@ -19,7 +19,7 @@ "data": { "password": "Passord" }, - "description": "Din tilgang har utl\u00f8pt eller blitt tilbakekalt. Skriv inn passordet ditt for \u00e5 koble kontoen din p\u00e5 nytt.", + "description": "Tilgangen din har utl\u00f8pt eller blitt tilbakekalt. Skriv inn passordet ditt for \u00e5 koble kontoen din til p\u00e5 nytt.", "title": "Godkjenne integrering p\u00e5 nytt" }, "user": { diff --git a/homeassistant/components/somfy_mylink/translations/hu.json b/homeassistant/components/somfy_mylink/translations/hu.json index 093a35b78fe..3610a930022 100644 --- a/homeassistant/components/somfy_mylink/translations/hu.json +++ b/homeassistant/components/somfy_mylink/translations/hu.json @@ -34,6 +34,7 @@ }, "init": { "data": { + "default_reverse": "A konfigur\u00e1latlan bor\u00edt\u00f3k alap\u00e9rtelmezett megford\u00edt\u00e1si \u00e1llapota", "entity_id": "Konfigur\u00e1ljon egy adott entit\u00e1st.", "target_id": "Az \u00e1rny\u00e9kol\u00f3 be\u00e1ll\u00edt\u00e1sainak konfigur\u00e1l\u00e1sa." }, diff --git a/homeassistant/components/sonos/translations/no.json b/homeassistant/components/sonos/translations/no.json index 2da0b5a1b0b..2e9b464f5f2 100644 --- a/homeassistant/components/sonos/translations/no.json +++ b/homeassistant/components/sonos/translations/no.json @@ -2,6 +2,7 @@ "config": { "abort": { "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "not_sonos_device": "Oppdaget enhet er ikke en Sonos -enhet", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "step": { diff --git a/homeassistant/components/switcher_kis/translations/no.json b/homeassistant/components/switcher_kis/translations/no.json new file mode 100644 index 00000000000..b3d6b5d782e --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/no.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "step": { + "confirm": { + "description": "Vil du starte oppsettet?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthru/translations/hu.json b/homeassistant/components/syncthru/translations/hu.json index b82b2587bc6..a5b645200db 100644 --- a/homeassistant/components/syncthru/translations/hu.json +++ b/homeassistant/components/syncthru/translations/hu.json @@ -5,6 +5,7 @@ }, "error": { "invalid_url": "\u00c9rv\u00e9nytelen URL", + "syncthru_not_supported": "Az eszk\u00f6z nem t\u00e1mogatja a SyncThru-t", "unknown_state": "A nyomtat\u00f3 \u00e1llapota ismeretlen, ellen\u0151rizze az URL-t \u00e9s a h\u00e1l\u00f3zati kapcsolatot" }, "flow_title": "{name}", diff --git a/homeassistant/components/synology_dsm/translations/no.json b/homeassistant/components/synology_dsm/translations/no.json index c8bb60bcb3e..d1e2d084f0d 100644 --- a/homeassistant/components/synology_dsm/translations/no.json +++ b/homeassistant/components/synology_dsm/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", @@ -29,6 +30,14 @@ "description": "Vil du konfigurere {name} ({host})?", "title": "" }, + "reauth": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "\u00c5rsak: {details}", + "title": "Synology DSM Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "host": "Vert", diff --git a/homeassistant/components/tesla/translations/no.json b/homeassistant/components/tesla/translations/no.json index ce706640636..11e49486107 100644 --- a/homeassistant/components/tesla/translations/no.json +++ b/homeassistant/components/tesla/translations/no.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "MFA -kode (valgfritt)", "password": "Passord", "username": "E-post" }, diff --git a/homeassistant/components/toon/translations/hu.json b/homeassistant/components/toon/translations/hu.json index 28a987a4512..18f333dccdf 100644 --- a/homeassistant/components/toon/translations/hu.json +++ b/homeassistant/components/toon/translations/hu.json @@ -15,6 +15,9 @@ }, "description": "V\u00e1lassza ki a hozz\u00e1adni k\u00edv\u00e1nt szerz\u0151d\u00e9sc\u00edmet.", "title": "V\u00e1lassza ki a meg\u00e1llapod\u00e1st" + }, + "pick_implementation": { + "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9shez" } } } diff --git a/homeassistant/components/tractive/translations/de.json b/homeassistant/components/tractive/translations/de.json new file mode 100644 index 00000000000..522649fe393 --- /dev/null +++ b/homeassistant/components/tractive/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "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/tractive/translations/no.json b/homeassistant/components/tractive/translations/no.json new file mode 100644 index 00000000000..3ae73c02103 --- /dev/null +++ b/homeassistant/components/tractive/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "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/uptimerobot/translations/de.json b/homeassistant/components/uptimerobot/translations/de.json index 81a9960b69c..7a50a5ba28e 100644 --- a/homeassistant/components/uptimerobot/translations/de.json +++ b/homeassistant/components/uptimerobot/translations/de.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "unknown": "Unerwarteter Fehler" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/uptimerobot/translations/no.json b/homeassistant/components/uptimerobot/translations/no.json new file mode 100644 index 00000000000..ee44ef0fdbc --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "unknown": "Uventet feil" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_api_key": "Ugyldig API-n\u00f8kkel", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.hu.json b/homeassistant/components/wolflink/translations/sensor.hu.json index 34f54e80ae8..0a257e570cf 100644 --- a/homeassistant/components/wolflink/translations/sensor.hu.json +++ b/homeassistant/components/wolflink/translations/sensor.hu.json @@ -3,6 +3,8 @@ "wolflink__state": { "1_x_warmwasser": "1 x DHW", "abgasklappe": "F\u00fcstg\u00e1zcsillap\u00edt\u00f3", + "absenkbetrieb": "Visszaes\u00e9s m\u00f3d", + "absenkstop": "Visszaes\u00e9s meg\u00e1ll\u00edt\u00e1sa", "aktiviert": "Aktiv\u00e1lt", "antilegionellenfunktion": "Anti-legionella funkci\u00f3", "at_abschaltung": "OT le\u00e1ll\u00edt\u00e1s", @@ -20,6 +22,7 @@ "dhw_prior": "DHW Priorit\u00e1s", "eco": "Takar\u00e9kos", "ein": "Enged\u00e9lyezve", + "estrichtrocknung": "Padl\u00f3sz\u00e1r\u00edt\u00e1si", "externe_deaktivierung": "K\u00fcls\u0151 deaktiv\u00e1l\u00e1s", "fernschalter_ein": "T\u00e1vir\u00e1ny\u00edt\u00f3 enged\u00e9lyezve", "frost_heizkreis": "F\u0171t\u0151k\u00f6r fagy\u00e1s", @@ -72,6 +75,7 @@ "tpw": "TPW", "urlaubsmodus": "Nyaral\u00e1s \u00fczemm\u00f3d", "ventilprufung": "Szelep teszt", + "vorspulen": "Bel\u00e9p\u00e9si sz\u00e1r\u00edt\u00e1s", "warmwasser": "DHW", "warmwasser_schnellstart": "DHW gyorsind\u00edt\u00e1s", "warmwasserbetrieb": "DHW m\u00f3d", diff --git a/homeassistant/components/xiaomi_miio/translations/select.no.json b/homeassistant/components/xiaomi_miio/translations/select.no.json new file mode 100644 index 00000000000..8205447ac2c --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.no.json @@ -0,0 +1,9 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "bright": "Lys", + "dim": "Dim", + "off": "Av" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/no.json b/homeassistant/components/yale_smart_alarm/translations/no.json index bbeedb7dc89..eba8861fa46 100644 --- a/homeassistant/components/yale_smart_alarm/translations/no.json +++ b/homeassistant/components/yale_smart_alarm/translations/no.json @@ -1,8 +1,15 @@ { "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert" + }, + "error": { + "invalid_auth": "Ugyldig godkjenning" + }, "step": { "reauth_confirm": { "data": { + "area_id": "Omr\u00e5de -ID", "name": "Navn", "password": "Passord", "username": "Brukernavn" @@ -10,6 +17,7 @@ }, "user": { "data": { + "area_id": "Omr\u00e5de -ID", "name": "Navn", "password": "Passord", "username": "Brukernavn" diff --git a/homeassistant/components/youless/translations/no.json b/homeassistant/components/youless/translations/no.json index 01ea5b65fb1..460c07cb535 100644 --- a/homeassistant/components/youless/translations/no.json +++ b/homeassistant/components/youless/translations/no.json @@ -1,8 +1,12 @@ { "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, "step": { "user": { "data": { + "host": "Vert", "name": "Navn" } } diff --git a/homeassistant/components/zha/translations/nn.json b/homeassistant/components/zha/translations/nn.json index 2e607435b7e..9e9b677ddc1 100644 --- a/homeassistant/components/zha/translations/nn.json +++ b/homeassistant/components/zha/translations/nn.json @@ -1,6 +1,9 @@ { "config": { "step": { + "port_config": { + "title": "Innstillinger" + }, "user": { "title": "ZHA" } diff --git a/homeassistant/components/zwave_js/translations/no.json b/homeassistant/components/zwave_js/translations/no.json index 8eb4c176356..34ddeb753b1 100644 --- a/homeassistant/components/zwave_js/translations/no.json +++ b/homeassistant/components/zwave_js/translations/no.json @@ -51,6 +51,21 @@ } } }, + "device_automation": { + "condition_type": { + "config_parameter": "Konfigurer parameter {subtype} verdi", + "node_status": "Nodestatus", + "value": "Gjeldende verdi for en Z-Wave-verdi" + }, + "trigger_type": { + "event.notification.entry_control": "Sendte et varsel om oppf\u00f8ringskontroll", + "event.notification.notification": "Sendte et varsel", + "event.value_notification.basic": "Grunnleggende CC -hendelse p\u00e5 {subtype}", + "event.value_notification.central_scene": "Sentral scenehandling p\u00e5 {subtype}", + "event.value_notification.scene_activation": "Sceneaktivering p\u00e5 {subtype}", + "state.node_status": "Nodestatus endret" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Kunne ikke hente oppdagelsesinformasjon om Z-Wave JS-tillegg", From 8b8f3b55b687e54c7cf9fca5ab9ccc9df968decb Mon Sep 17 00:00:00 2001 From: Gian Klug <51193103+gianklug@users.noreply.github.com> Date: Sat, 7 Aug 2021 06:25:35 +0200 Subject: [PATCH 021/355] Add state class and last reset in kostal_plenticore (#54084) * Add state class and implement in kostal_plenticore * Add support for more entity variants * Add the state_class to the total values too * Reformat kostal const.py * Add `last_reset` to kostal_plenticore entities when `state_class` is set Also reformat sensor.py * Fix import * Remove the constants from the homeassistant constants file * Use sensor constants for the state_class * Reformat * Reformat * Move last_reset from sensor.py into const.py * Remove last_reset on PERCENTAGE entities * Address lint issues * Update homeassistant/components/kostal_plenticore/sensor.py Co-authored-by: Martin Hjelmare * Import datetime * Apply suggestions from code review * Fix isort * Fix more isort Co-authored-by: Martin Hjelmare --- .../components/kostal_plenticore/const.py | 95 ++++++++++++++++--- .../components/kostal_plenticore/sensor.py | 18 +++- 2 files changed, 99 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/const.py b/homeassistant/components/kostal_plenticore/const.py index 5c223f4f5d6..ede8e10cb25 100644 --- a/homeassistant/components/kostal_plenticore/const.py +++ b/homeassistant/components/kostal_plenticore/const.py @@ -1,5 +1,10 @@ """Constants for the Kostal Plenticore Solar Inverter integration.""" +from homeassistant.components.sensor import ( + ATTR_LAST_RESET, + ATTR_STATE_CLASS, + STATE_CLASS_MEASUREMENT, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, @@ -11,11 +16,14 @@ from homeassistant.const import ( PERCENTAGE, POWER_WATT, ) +from homeassistant.util.dt import utc_from_timestamp DOMAIN = "kostal_plenticore" ATTR_ENABLED_DEFAULT = "entity_registry_enabled_default" +LAST_RESET_NEVER = utc_from_timestamp(0) + # Defines all entities for process data. # # Each entry is defined with a tuple of these values: @@ -40,6 +48,7 @@ SENSOR_PROCESS_DATA = [ ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, ATTR_ENABLED_DEFAULT: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, "format_round", ), @@ -51,6 +60,7 @@ SENSOR_PROCESS_DATA = [ ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, ATTR_ENABLED_DEFAULT: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, "format_round", ), @@ -65,28 +75,44 @@ SENSOR_PROCESS_DATA = [ "devices:local", "HomeGrid_P", "Home Power from Grid", - {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, "format_round", ), ( "devices:local", "HomeOwn_P", "Home Power from Own", - {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, "format_round", ), ( "devices:local", "HomePv_P", "Home Power from PV", - {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, "format_round", ), ( "devices:local", "Home_P", "Home Power", - {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, "format_round", ), ( @@ -97,6 +123,7 @@ SENSOR_PROCESS_DATA = [ ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, ATTR_ENABLED_DEFAULT: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, "format_round", ), @@ -104,28 +131,44 @@ SENSOR_PROCESS_DATA = [ "devices:local:pv1", "P", "DC1 Power", - {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, "format_round", ), ( "devices:local:pv2", "P", "DC2 Power", - {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, "format_round", ), ( "devices:local:pv3", "P", "DC3 Power", - {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, "format_round", ), ( "devices:local", "PV2Bat_P", "PV to Battery Power", - {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, "format_round", ), ( @@ -139,14 +182,18 @@ SENSOR_PROCESS_DATA = [ "devices:local:battery", "Cycles", "Battery Cycles", - {ATTR_ICON: "mdi:recycle"}, + {ATTR_ICON: "mdi:recycle", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT}, "format_round", ), ( "devices:local:battery", "P", "Battery Power", - {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, "format_round", ), ( @@ -174,7 +221,11 @@ SENSOR_PROCESS_DATA = [ "scb:statistic:EnergyFlow", "Statistic:Autarky:Total", "Autarky Total", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + { + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + ATTR_ICON: "mdi:chart-donut", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, "format_round", ), ( @@ -202,7 +253,11 @@ SENSOR_PROCESS_DATA = [ "scb:statistic:EnergyFlow", "Statistic:OwnConsumptionRate:Total", "Own Consumption Rate Total", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + { + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + ATTR_ICON: "mdi:chart-donut", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, "format_round", ), ( @@ -249,6 +304,8 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_LAST_RESET: LAST_RESET_NEVER, }, "format_energy", ), @@ -289,6 +346,8 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_LAST_RESET: LAST_RESET_NEVER, }, "format_energy", ), @@ -329,6 +388,8 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_LAST_RESET: LAST_RESET_NEVER, }, "format_energy", ), @@ -369,6 +430,8 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_LAST_RESET: LAST_RESET_NEVER, }, "format_energy", ), @@ -409,6 +472,8 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_LAST_RESET: LAST_RESET_NEVER, }, "format_energy", ), @@ -449,6 +514,8 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_LAST_RESET: LAST_RESET_NEVER, }, "format_energy", ), @@ -489,6 +556,8 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_LAST_RESET: LAST_RESET_NEVER, }, "format_energy", ), @@ -530,6 +599,8 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_LAST_RESET: LAST_RESET_NEVER, }, "format_energy", ), diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py index 717dfacbfdf..099d359e619 100644 --- a/homeassistant/components/kostal_plenticore/sensor.py +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -1,11 +1,15 @@ """Platform for Kostal Plenticore sensors.""" from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta import logging from typing import Any, Callable -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + ATTR_LAST_RESET, + ATTR_STATE_CLASS, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant @@ -179,11 +183,21 @@ class PlenticoreDataSensor(CoordinatorEntity, SensorEntity): """Return the class of this device, from component DEVICE_CLASSES.""" return self._sensor_data.get(ATTR_DEVICE_CLASS) + @property + def state_class(self) -> str | None: + """Return the class of the state of this device, from component STATE_CLASSES.""" + return self._sensor_data.get(ATTR_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._sensor_data.get(ATTR_ENABLED_DEFAULT, False) + @property + def last_reset(self) -> datetime | None: + """Return the last_reset time.""" + return self._sensor_data.get(ATTR_LAST_RESET) + @property def state(self) -> Any | None: """Return the state of the sensor.""" From 0b52e13eb8178dba45ebd3d8eb75a5e47d9d2d96 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 7 Aug 2021 01:18:08 -0400 Subject: [PATCH 022/355] Fix androidtv media_image_hash (#54188) --- homeassistant/components/androidtv/media_player.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 08ae2999e37..8bc53bd86b7 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -449,6 +449,11 @@ class ADBDevice(MediaPlayerEntity): ATTR_HDMI_INPUT: None, } + @property + def media_image_hash(self): + """Hash value for media image.""" + return f"{datetime.now().timestamp()}" if self._screencap else None + @adb_decorator() async def _adb_screencap(self): """Take a screen capture from the device.""" @@ -458,9 +463,6 @@ class ADBDevice(MediaPlayerEntity): """Fetch current playing image.""" if not self._screencap or self.state in (STATE_OFF, None) or not self.available: return None, None - self._attr_media_image_hash = ( - f"{datetime.now().timestamp()}" if self._screencap else None - ) media_data = await self._adb_screencap() if media_data: From 6830eec549c372946b19035000c10afecd2f2da3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20R=C3=B8rvik?= <60797691+jorgror@users.noreply.github.com> Date: Sat, 7 Aug 2021 09:45:53 +0200 Subject: [PATCH 023/355] Flexit component fix for updated modbus (#53583) * pyflexit first argument should be a ModbusSerialClient This component broke with 2021.6 I have tested this patch on my setup and it restores functionality * Implemented async reading of modbus values Stopped using pyflexit as this is outdated and not needed Instead using async_pymodbus_call from ModbusHub class * Bugfix: Reading fan mode from wrong register * Implemented async writing Set target temperature and fan mode using modbus call Added some error handling * No longer require pyflexit * Review comments. Co-authored-by: jan Iversen --- homeassistant/components/flexit/climate.py | 149 ++++++++++++++---- homeassistant/components/flexit/manifest.json | 1 - requirements_all.txt | 3 - 3 files changed, 119 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index cf4662b9866..5e7ac137982 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -3,7 +3,6 @@ from __future__ import annotations import logging -from pyflexit.pyflexit import pyflexit import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity @@ -12,7 +11,15 @@ from homeassistant.components.climate.const import ( SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.components.modbus.const import CONF_HUB, DEFAULT_HUB, MODBUS_DOMAIN +from homeassistant.components.modbus.const import ( + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_REGISTER_INPUT, + CALL_TYPE_WRITE_REGISTER, + CONF_HUB, + DEFAULT_HUB, + MODBUS_DOMAIN, +) +from homeassistant.components.modbus.modbus import ModbusHub from homeassistant.const import ( ATTR_TEMPERATURE, CONF_NAME, @@ -20,7 +27,9 @@ from homeassistant.const import ( DEVICE_DEFAULT_NAME, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -35,18 +44,25 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities, + discovery_info: DiscoveryInfoType = None, +): """Set up the Flexit Platform.""" modbus_slave = config.get(CONF_SLAVE) name = config.get(CONF_NAME) hub = hass.data[MODBUS_DOMAIN][config.get(CONF_HUB)] - add_entities([Flexit(hub, modbus_slave, name)], True) + async_add_entities([Flexit(hub, modbus_slave, name)], True) class Flexit(ClimateEntity): """Representation of a Flexit AC unit.""" - def __init__(self, hub, modbus_slave, name): + def __init__( + self, hub: ModbusHub, modbus_slave: int | None, name: str | None + ) -> None: """Initialize the unit.""" self._hub = hub self._name = name @@ -64,34 +80,65 @@ class Flexit(ClimateEntity): self._heating = None self._cooling = None self._alarm = False - self.unit = pyflexit(hub, modbus_slave) + self._outdoor_air_temp = None @property def supported_features(self): """Return the list of supported features.""" return SUPPORT_FLAGS - def update(self): + async def async_update(self): """Update unit attributes.""" - if not self.unit.update(): - _LOGGER.warning("Modbus read failed") + self._target_temperature = await self._async_read_temp_from_register( + CALL_TYPE_REGISTER_HOLDING, 8 + ) + self._current_temperature = await self._async_read_temp_from_register( + CALL_TYPE_REGISTER_INPUT, 9 + ) + res = await self._async_read_int16_from_register(CALL_TYPE_REGISTER_HOLDING, 17) + if res < len(self._fan_modes): + self._current_fan_mode = res + self._filter_hours = await self._async_read_int16_from_register( + CALL_TYPE_REGISTER_INPUT, 8 + ) + # # Mechanical heat recovery, 0-100% + self._heat_recovery = await self._async_read_int16_from_register( + CALL_TYPE_REGISTER_INPUT, 14 + ) + # # Heater active 0-100% + self._heating = await self._async_read_int16_from_register( + CALL_TYPE_REGISTER_INPUT, 15 + ) + # # Cooling active 0-100% + self._cooling = await self._async_read_int16_from_register( + CALL_TYPE_REGISTER_INPUT, 13 + ) + # # Filter alarm 0/1 + self._filter_alarm = await self._async_read_int16_from_register( + CALL_TYPE_REGISTER_INPUT, 27 + ) + # # Heater enabled or not. Does not mean it's necessarily heating + self._heater_enabled = await self._async_read_int16_from_register( + CALL_TYPE_REGISTER_INPUT, 28 + ) + self._outdoor_air_temp = await self._async_read_temp_from_register( + CALL_TYPE_REGISTER_INPUT, 11 + ) - self._target_temperature = self.unit.get_target_temp - self._current_temperature = self.unit.get_temp - self._current_fan_mode = self._fan_modes[self.unit.get_fan_speed] - self._filter_hours = self.unit.get_filter_hours - # Mechanical heat recovery, 0-100% - self._heat_recovery = self.unit.get_heat_recovery - # Heater active 0-100% - self._heating = self.unit.get_heating - # Cooling active 0-100% - self._cooling = self.unit.get_cooling - # Filter alarm 0/1 - self._filter_alarm = self.unit.get_filter_alarm - # Heater enabled or not. Does not mean it's necessarily heating - self._heater_enabled = self.unit.get_heater_enabled - # Current operation mode - self._current_operation = self.unit.get_operation + actual_air_speed = await self._async_read_int16_from_register( + CALL_TYPE_REGISTER_INPUT, 48 + ) + + if self._heating: + self._current_operation = "Heating" + elif self._cooling: + self._current_operation = "Cooling" + elif self._heat_recovery: + self._current_operation = "Recovering" + elif actual_air_speed: + self._current_operation = "Fan Only" + else: + self._current_operation = "Off" @property def extra_state_attributes(self): @@ -103,6 +150,7 @@ class Flexit(ClimateEntity): "heating": self._heating, "heater_enabled": self._heater_enabled, "cooling": self._cooling, + "outdoor_air_temp": self._outdoor_air_temp, } @property @@ -153,12 +201,53 @@ class Flexit(ClimateEntity): """Return the list of available fan modes.""" return self._fan_modes - def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperature.""" if kwargs.get(ATTR_TEMPERATURE) is not None: - self._target_temperature = kwargs.get(ATTR_TEMPERATURE) - self.unit.set_temp(self._target_temperature) + target_temperature = kwargs.get(ATTR_TEMPERATURE) + else: + _LOGGER.error("Received invalid temperature") + return - def set_fan_mode(self, fan_mode): + if await self._async_write_int16_to_register(8, target_temperature * 10): + self._target_temperature = target_temperature + else: + _LOGGER.error("Modbus error setting target temperature to Flexit") + + async def async_set_fan_mode(self, fan_mode): """Set new fan mode.""" - self.unit.set_fan_speed(self._fan_modes.index(fan_mode)) + if await self._async_write_int16_to_register( + 17, self.fan_modes.index(fan_mode) + ): + self._current_fan_mode = self.fan_modes.index(fan_mode) + else: + _LOGGER.error("Modbus error setting fan mode to Flexit") + + # Based on _async_read_register in ModbusThermostat class + async def _async_read_int16_from_register(self, register_type, register) -> int: + """Read register using the Modbus hub slave.""" + result = await self._hub.async_pymodbus_call( + self._slave, register, 1, register_type + ) + if result is None: + _LOGGER.error("Error reading value from Flexit modbus adapter") + return -1 + + return int(result.registers[0]) + + async def _async_read_temp_from_register(self, register_type, register) -> float: + result = float( + await self._async_read_int16_from_register(register_type, register) + ) + if result == -1: + return -1 + return result / 10.0 + + async def _async_write_int16_to_register(self, register, value) -> bool: + value = int(value) + result = await self._hub.async_pymodbus_call( + self._slave, register, value, CALL_TYPE_WRITE_REGISTER + ) + if result == -1: + return False + return True diff --git a/homeassistant/components/flexit/manifest.json b/homeassistant/components/flexit/manifest.json index 96ed5b55904..d9f84d5ab81 100644 --- a/homeassistant/components/flexit/manifest.json +++ b/homeassistant/components/flexit/manifest.json @@ -2,7 +2,6 @@ "domain": "flexit", "name": "Flexit", "documentation": "https://www.home-assistant.io/integrations/flexit", - "requirements": ["pyflexit==0.3"], "dependencies": ["modbus"], "codeowners": [], "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 5dc4d86d603..a1cf52d2071 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1437,9 +1437,6 @@ pyfido==2.1.1 # homeassistant.components.fireservicerota pyfireservicerota==0.0.43 -# homeassistant.components.flexit -pyflexit==0.3 - # homeassistant.components.flic pyflic==2.0.3 From 422fe48c3a438ba94ba1da83ed4038d3581c70cb Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 7 Aug 2021 12:15:31 +0200 Subject: [PATCH 024/355] Correct device class typo in rfxtrx (#54200) --- homeassistant/components/rfxtrx/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index d697c56f7e8..f6751d760b2 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -111,7 +111,7 @@ async def async_setup_entry( if description is None: description = BinarySensorEntityDescription(key=type_string) if device_class: - description = replace(description, device_class=device) + description = replace(description, device_class=device_class) return description for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): From 6dd875bc4a9b53f3ff7858d1a7486d89d6d6e56d Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 7 Aug 2021 12:17:33 +0200 Subject: [PATCH 025/355] Bump ha-philipsjs to 2.7.5 (#54176) --- homeassistant/components/philips_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index 4f3ee5a9ab3..3bea3ff7337 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -2,7 +2,7 @@ "domain": "philips_js", "name": "Philips TV", "documentation": "https://www.home-assistant.io/integrations/philips_js", - "requirements": ["ha-philipsjs==2.7.4"], + "requirements": ["ha-philipsjs==2.7.5"], "codeowners": ["@elupus"], "config_flow": true, "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index a1cf52d2071..d4d958a529b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -744,7 +744,7 @@ guppy3==3.1.0 ha-ffmpeg==3.0.2 # homeassistant.components.philips_js -ha-philipsjs==2.7.4 +ha-philipsjs==2.7.5 # homeassistant.components.habitica habitipy==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83dfc9695de..f8e66439b42 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -422,7 +422,7 @@ guppy3==3.1.0 ha-ffmpeg==3.0.2 # homeassistant.components.philips_js -ha-philipsjs==2.7.4 +ha-philipsjs==2.7.5 # homeassistant.components.habitica habitipy==0.2.0 From e0bc911e2487766388b2d0c1f20be48660478747 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 7 Aug 2021 12:22:08 +0200 Subject: [PATCH 026/355] Fix Neato reauth flow when token expired (#52843) * Fix Neato reauth flow when token expired * Change and simplify approach * Missing file * Cleanup * Update unique_id * Added missing lamda * Unique_id reworked * Guard for id future ID changes * Bump pybotvac: provide unique_id * Address review comment * Fix update check * Remove token check * Trigger reauth only for 401 and 403 code response * Review comments --- .coveragerc | 1 + homeassistant/components/neato/__init__.py | 47 ++++++------------- homeassistant/components/neato/config_flow.py | 6 +-- homeassistant/components/neato/hub.py | 47 +++++++++++++++++++ homeassistant/components/neato/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 68 insertions(+), 39 deletions(-) create mode 100644 homeassistant/components/neato/hub.py diff --git a/.coveragerc b/.coveragerc index 1a9221b3abb..839bd34f6e5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -677,6 +677,7 @@ omit = homeassistant/components/neato/__init__.py homeassistant/components/neato/api.py homeassistant/components/neato/camera.py + homeassistant/components/neato/hub.py homeassistant/components/neato/sensor.py homeassistant/components/neato/switch.py homeassistant/components/neato/vacuum.py diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index 28569e0f1d7..2c277a2ac8d 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -1,7 +1,7 @@ """Support for Neato botvac connected vacuum cleaners.""" -from datetime import timedelta import logging +import aiohttp from pybotvac import Account, Neato from pybotvac.exceptions import NeatoException import voluptuous as vol @@ -12,17 +12,10 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from homeassistant.helpers.typing import ConfigType -from homeassistant.util import Throttle from . import api, config_flow -from .const import ( - NEATO_CONFIG, - NEATO_DOMAIN, - NEATO_LOGIN, - NEATO_MAP_DATA, - NEATO_PERSISTENT_MAPS, - NEATO_ROBOTS, -) +from .const import NEATO_CONFIG, NEATO_DOMAIN, NEATO_LOGIN +from .hub import NeatoHub _LOGGER = logging.getLogger(__name__) @@ -77,10 +70,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ) + session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + try: + await session.async_ensure_token_valid() + except aiohttp.ClientResponseError as ex: + _LOGGER.debug("API error: %s (%s)", ex.code, ex.message) + if ex.code in (401, 403): + raise ConfigEntryAuthFailed("Token not valid, trigger renewal") from ex + neato_session = api.ConfigEntryAuth(hass, entry, implementation) hass.data[NEATO_DOMAIN][entry.entry_id] = neato_session hub = NeatoHub(hass, Account(neato_session)) + await hub.async_update_entry_unique_id(entry) + try: await hass.async_add_executor_job(hub.update_robots) except NeatoException as ex: @@ -94,32 +97,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigType) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[NEATO_DOMAIN].pop(entry.entry_id) return unload_ok - - -class NeatoHub: - """A My Neato hub wrapper class.""" - - def __init__(self, hass: HomeAssistant, neato: Account) -> None: - """Initialize the Neato hub.""" - self._hass = hass - self.my_neato: Account = neato - - @Throttle(timedelta(minutes=1)) - def update_robots(self): - """Update the robot states.""" - _LOGGER.debug("Running HUB.update_robots %s", self._hass.data.get(NEATO_ROBOTS)) - self._hass.data[NEATO_ROBOTS] = self.my_neato.robots - self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps - self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps - - def download_map(self, url): - """Download a new map image.""" - map_image_data = self.my_neato.get_map_image(url) - return map_image_data diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py index 580faffe8ff..c4ca9e45a89 100644 --- a/homeassistant/components/neato/config_flow.py +++ b/homeassistant/components/neato/config_flow.py @@ -5,7 +5,7 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_TOKEN +from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.helpers import config_entry_oauth2_flow from .const import NEATO_DOMAIN @@ -26,7 +26,7 @@ class OAuth2FlowHandler( async def async_step_user(self, user_input: dict | None = None) -> dict: """Create an entry for the flow.""" current_entries = self._async_current_entries() - if current_entries and CONF_TOKEN in current_entries[0].data: + if self.source != SOURCE_REAUTH and current_entries: # Already configured return self.async_abort(reason="already_configured") @@ -47,7 +47,7 @@ class OAuth2FlowHandler( async def async_oauth_create_entry(self, data: dict) -> dict: """Create an entry for the flow. Update an entry if one already exist.""" current_entries = self._async_current_entries() - if current_entries and CONF_TOKEN not in current_entries[0].data: + if self.source == SOURCE_REAUTH and current_entries: # Update entry self.hass.config_entries.async_update_entry( current_entries[0], title=self.flow_impl.name, data=data diff --git a/homeassistant/components/neato/hub.py b/homeassistant/components/neato/hub.py new file mode 100644 index 00000000000..b394507f408 --- /dev/null +++ b/homeassistant/components/neato/hub.py @@ -0,0 +1,47 @@ +"""Support for Neato botvac connected vacuum cleaners.""" +from datetime import timedelta +import logging + +from pybotvac import Account + +from homeassistant.core import HomeAssistant +from homeassistant.util import Throttle + +from .const import NEATO_MAP_DATA, NEATO_PERSISTENT_MAPS, NEATO_ROBOTS + +_LOGGER = logging.getLogger(__name__) + + +class NeatoHub: + """A My Neato hub wrapper class.""" + + def __init__(self, hass: HomeAssistant, neato: Account) -> None: + """Initialize the Neato hub.""" + self._hass = hass + self.my_neato: Account = neato + + @Throttle(timedelta(minutes=1)) + def update_robots(self): + """Update the robot states.""" + _LOGGER.debug("Running HUB.update_robots %s", self._hass.data.get(NEATO_ROBOTS)) + self._hass.data[NEATO_ROBOTS] = self.my_neato.robots + self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps + self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps + + def download_map(self, url): + """Download a new map image.""" + map_image_data = self.my_neato.get_map_image(url) + return map_image_data + + async def async_update_entry_unique_id(self, entry) -> str: + """Update entry for unique_id.""" + + await self._hass.async_add_executor_job(self.my_neato.refresh_userdata) + unique_id = self.my_neato.unique_id + + if entry.unique_id == unique_id: + return unique_id + + _LOGGER.debug("Updating user unique_id for previous config entry") + self._hass.config_entries.async_update_entry(entry, unique_id=unique_id) + return unique_id diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index 014e366db46..fc751df45de 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -3,7 +3,7 @@ "name": "Neato Botvac", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/neato", - "requirements": ["pybotvac==0.0.21"], + "requirements": ["pybotvac==0.0.22"], "codeowners": ["@dshokouhi", "@Santobert"], "dependencies": ["http"], "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index d4d958a529b..7a4f1153d70 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1342,7 +1342,7 @@ pyblackbird==0.5 # pybluez==0.22 # homeassistant.components.neato -pybotvac==0.0.21 +pybotvac==0.0.22 # homeassistant.components.nissan_leaf pycarwings2==2.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8e66439b42..bbd9cbbc7ff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -760,7 +760,7 @@ pyatv==0.8.2 pyblackbird==0.5 # homeassistant.components.neato -pybotvac==0.0.21 +pybotvac==0.0.22 # homeassistant.components.cloudflare pycfdns==1.2.1 From ca2bdfab6b7701e4fc250387a82ef1dfde93dd6f Mon Sep 17 00:00:00 2001 From: Mk4242 <76903406+Mk4242@users.noreply.github.com> Date: Sat, 7 Aug 2021 18:24:19 +0200 Subject: [PATCH 027/355] Update const.py (#54195) Remove extra attribute for FlowTemperature sensor, which prevents the ebusd integration from initialising --- homeassistant/components/ebusd/const.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/ebusd/const.py b/homeassistant/components/ebusd/const.py index 7052a9950fd..3d4ab508ca2 100644 --- a/homeassistant/components/ebusd/const.py +++ b/homeassistant/components/ebusd/const.py @@ -223,7 +223,6 @@ SENSOR_TYPES = { None, 4, DEVICE_CLASS_TEMPERATURE, - None, ], "Flame": ["Flame", None, "mdi:toggle-switch", 2, None], "PowerEnergyConsumptionHeatingCircuit": [ From 819131ad21ef2516028f73a26d2c0c49660d8c66 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 7 Aug 2021 19:15:25 +0200 Subject: [PATCH 028/355] Raise ConfigEntryNotReady for Neato API error (#54227) --- homeassistant/components/neato/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index 2c277a2ac8d..6310e81cdd0 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -77,6 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug("API error: %s (%s)", ex.code, ex.message) if ex.code in (401, 403): raise ConfigEntryAuthFailed("Token not valid, trigger renewal") from ex + raise ConfigEntryNotReady from ex neato_session = api.ConfigEntryAuth(hass, entry, implementation) hass.data[NEATO_DOMAIN][entry.entry_id] = neato_session From 3b1d44478a0ecefdef82e8d5e6b5f39e8b85eaa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 7 Aug 2021 20:22:02 +0200 Subject: [PATCH 029/355] Change update interval from 60s to 10s for Uptime Robot (#54230) --- homeassistant/components/uptimerobot/const.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/uptimerobot/const.py b/homeassistant/components/uptimerobot/const.py index ee9832a040a..7f3655b75cf 100644 --- a/homeassistant/components/uptimerobot/const.py +++ b/homeassistant/components/uptimerobot/const.py @@ -7,7 +7,8 @@ from typing import Final LOGGER: Logger = getLogger(__package__) -COORDINATOR_UPDATE_INTERVAL: timedelta = timedelta(seconds=60) +# The free plan is limited to 10 requests/minute +COORDINATOR_UPDATE_INTERVAL: timedelta = timedelta(seconds=10) DOMAIN: Final = "uptimerobot" PLATFORMS: Final = ["binary_sensor"] From a485b14293588bf5d82bf64d391f447ff5743936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 7 Aug 2021 20:22:19 +0200 Subject: [PATCH 030/355] Set entities as unavailable if last update was not successful (#54229) --- homeassistant/components/uptimerobot/entity.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/uptimerobot/entity.py b/homeassistant/components/uptimerobot/entity.py index 4b4847dfc7c..b265af77535 100644 --- a/homeassistant/components/uptimerobot/entity.py +++ b/homeassistant/components/uptimerobot/entity.py @@ -75,4 +75,6 @@ class UptimeRobotEntity(CoordinatorEntity): @property def available(self) -> bool: """Returtn if entity is available.""" + if not self.coordinator.last_update_success: + return False return self.monitor is not None From af565ea6bd8bc88cd10b5a6f1d979a9e121c74cc Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 8 Aug 2021 00:11:15 +0000 Subject: [PATCH 031/355] [ci skip] Translation update --- .../adguard/translations/zh-Hans.json | 13 ++++- .../agent_dvr/translations/zh-Hans.json | 13 +++++ .../asuswrt/translations/zh-Hans.json | 44 ++++++++++++++++ .../brother/translations/zh-Hans.json | 15 ++++++ .../cert_expiry/translations/zh-Hans.json | 13 +++-- .../co2signal/translations/zh-Hans.json | 11 ++++ .../coronavirus/translations/zh-Hans.json | 3 +- .../components/energy/translations/es.json | 3 ++ .../ezviz/translations/zh-Hans.json | 52 +++++++++++++++++++ .../components/flipr/translations/es.json | 15 ++++++ .../glances/translations/zh-Hans.json | 26 ++++++++-- .../components/gree/translations/zh-Hans.json | 13 +++++ .../homeassistant/translations/es.json | 1 + .../homeassistant/translations/zh-Hans.json | 1 + .../translations/zh-Hans.json | 2 + .../huawei_lte/translations/zh-Hans.json | 3 +- .../components/ipp/translations/zh-Hans.json | 19 ++++++- .../components/kodi/translations/zh-Hans.json | 41 ++++++++++++++- .../mikrotik/translations/zh-Hans.json | 23 +++++++- .../translations/zh-Hans.json | 22 ++++++++ .../onvif/translations/zh-Hans.json | 52 ++++++++++++++++++- .../components/plex/translations/zh-Hans.json | 38 +++++++++++++- .../components/prosegur/translations/es.json | 29 +++++++++++ .../components/prosegur/translations/pt.json | 19 +++++++ .../components/renault/translations/es.json | 19 +++++++ .../renault/translations/zh-Hans.json | 26 ++++++++++ .../samsungtv/translations/zh-Hans.json | 34 ++++++++++++ .../solaredge/translations/zh-Hans.json | 16 +++++- .../spotify/translations/zh-Hans.json | 19 +++++++ .../syncthing/translations/zh-Hans.json | 22 ++++++++ .../syncthru/translations/zh-Hans.json | 7 +++ .../synology_dsm/translations/zh-Hans.json | 12 ++++- .../components/tractive/translations/es.json | 19 +++++++ .../components/tractive/translations/he.json | 19 +++++++ .../components/tractive/translations/pt.json | 16 ++++++ .../tractive/translations/zh-Hans.json | 19 +++++++ .../transmission/translations/zh-Hans.json | 28 +++++++++- .../uptimerobot/translations/es.json | 20 +++++++ .../uptimerobot/translations/he.json | 4 +- .../uptimerobot/translations/pt.json | 10 ++++ .../uptimerobot/translations/zh-Hans.json | 20 +++++++ .../vizio/translations/zh-Hans.json | 12 +++++ .../xiaomi_miio/translations/select.es.json | 9 ++++ .../translations/select.zh-Hans.json | 9 ++++ .../yale_smart_alarm/translations/es.json | 28 ++++++++++ .../components/youless/translations/es.json | 15 ++++++ 46 files changed, 830 insertions(+), 24 deletions(-) create mode 100644 homeassistant/components/asuswrt/translations/zh-Hans.json create mode 100644 homeassistant/components/co2signal/translations/zh-Hans.json create mode 100644 homeassistant/components/energy/translations/es.json create mode 100644 homeassistant/components/ezviz/translations/zh-Hans.json create mode 100644 homeassistant/components/flipr/translations/es.json create mode 100644 homeassistant/components/gree/translations/zh-Hans.json create mode 100644 homeassistant/components/minecraft_server/translations/zh-Hans.json create mode 100644 homeassistant/components/prosegur/translations/es.json create mode 100644 homeassistant/components/prosegur/translations/pt.json create mode 100644 homeassistant/components/renault/translations/es.json create mode 100644 homeassistant/components/renault/translations/zh-Hans.json create mode 100644 homeassistant/components/samsungtv/translations/zh-Hans.json create mode 100644 homeassistant/components/syncthing/translations/zh-Hans.json create mode 100644 homeassistant/components/syncthru/translations/zh-Hans.json create mode 100644 homeassistant/components/tractive/translations/es.json create mode 100644 homeassistant/components/tractive/translations/he.json create mode 100644 homeassistant/components/tractive/translations/pt.json create mode 100644 homeassistant/components/tractive/translations/zh-Hans.json create mode 100644 homeassistant/components/uptimerobot/translations/es.json create mode 100644 homeassistant/components/uptimerobot/translations/pt.json create mode 100644 homeassistant/components/uptimerobot/translations/zh-Hans.json create mode 100644 homeassistant/components/vizio/translations/zh-Hans.json create mode 100644 homeassistant/components/xiaomi_miio/translations/select.es.json create mode 100644 homeassistant/components/xiaomi_miio/translations/select.zh-Hans.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/es.json create mode 100644 homeassistant/components/youless/translations/es.json diff --git a/homeassistant/components/adguard/translations/zh-Hans.json b/homeassistant/components/adguard/translations/zh-Hans.json index 4204beb5268..ee68ce83e91 100644 --- a/homeassistant/components/adguard/translations/zh-Hans.json +++ b/homeassistant/components/adguard/translations/zh-Hans.json @@ -1,14 +1,23 @@ { "config": { "abort": { + "already_configured": "\u670d\u52a1\u5df2\u88ab\u914d\u7f6e", "existing_instance_updated": "\u66f4\u65b0\u4e86\u73b0\u6709\u914d\u7f6e\u3002" }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + }, "step": { "user": { "data": { + "host": "\u4e3b\u673a\u5730\u5740", "password": "\u5bc6\u7801", - "username": "\u7528\u6237\u540d" - } + "port": "\u7aef\u53e3", + "ssl": "\u4f7f\u7528 SSL \u8bc1\u4e66\u51ed\u8bc1", + "username": "\u7528\u6237\u540d", + "verify_ssl": "\u9a8c\u8bc1 SSL \u8bc1\u4e66\u51ed\u8bc1" + }, + "description": "\u8bbe\u7f6e\u60a8\u7684 AdGuard Home \u5b9e\u4f8b\u4ee5\u5141\u8bb8\u76d1\u89c6\u548c\u63a7\u5236" } } } diff --git a/homeassistant/components/agent_dvr/translations/zh-Hans.json b/homeassistant/components/agent_dvr/translations/zh-Hans.json index 2941dfd9383..68393fce470 100644 --- a/homeassistant/components/agent_dvr/translations/zh-Hans.json +++ b/homeassistant/components/agent_dvr/translations/zh-Hans.json @@ -1,7 +1,20 @@ { "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e" + }, "error": { + "already_in_progress": "\u914d\u7f6e\u6d41\u5df2\u8fdb\u884c\u4e2d", "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "port": "\u7aef\u53e3" + }, + "title": "\u914d\u7f6e Agent DVR" + } } } } \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/zh-Hans.json b/homeassistant/components/asuswrt/translations/zh-Hans.json new file mode 100644 index 00000000000..69f7bf98df3 --- /dev/null +++ b/homeassistant/components/asuswrt/translations/zh-Hans.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86\uff0c\u4e14\u53ea\u80fd\u914d\u7f6e\u4e00\u6b21\u3002" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_host": "\u65e0\u6548\u7684\u4e3b\u673a\u5730\u5740\u6216 IP \u5730\u5740", + "pwd_and_ssh": "\u53ea\u63d0\u4f9b\u5bc6\u7801\u6216 SSH \u5bc6\u94a5\u6587\u4ef6", + "pwd_or_ssh": "\u8bf7\u63d0\u4f9b\u5bc6\u7801\u6216 SSH \u5bc6\u94a5\u6587\u4ef6", + "ssh_not_file": "\u672a\u627e\u5230 SSH \u5bc6\u94a5\u6587\u4ef6", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "mode": "\u4f7f\u7528\u6a21\u5f0f", + "name": "\u540d\u79f0", + "password": "\u5bc6\u7801", + "port": "\u7aef\u53e3", + "protocol": "\u901a\u4fe1\u534f\u8bae", + "ssh_key": "SSH \u5bc6\u94a5\u6587\u4ef6\u8def\u5f84 (\u4e0d\u662f\u5bc6\u7801)", + "username": "\u7528\u6237\u540d" + }, + "description": "\u8bbe\u7f6e\u8fde\u63a5\u5230\u8def\u7531\u5668\u6240\u9700\u7684\u53c2\u6570", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "\u7b49\u5f85\u591a\u5c11\u79d2\u540e\u5219\u5224\u5b9a\u8bbe\u5907\u79bb\u5f00", + "dnsmasq": "\u8def\u7531\u5668\u4e2d\u7684 dnsmasq.leases \u6587\u4ef6\u4f4d\u7f6e", + "interface": "\u60f3\u8981\u76d1\u6d4b\u7684\u7aef\u53e3(\u4f8b\u5982: eth0,eth1 \u7b49)", + "require_ip": "\u8bbe\u5907\u5fc5\u987b\u5177\u6709 IP (\u7528\u4e8e\u63a5\u5165\u70b9\u6a21\u5f0f)" + }, + "title": "AsusWRT \u9009\u9879" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/translations/zh-Hans.json b/homeassistant/components/brother/translations/zh-Hans.json index 8f9e85e54a9..91e0c310dd1 100644 --- a/homeassistant/components/brother/translations/zh-Hans.json +++ b/homeassistant/components/brother/translations/zh-Hans.json @@ -1,8 +1,23 @@ { "config": { + "abort": { + "unsupported_model": "\u4e0d\u652f\u6301\u6b64\u6253\u5370\u673a\u578b\u53f7\u3002" + }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", "snmp_error": "SNMP\u670d\u52a1\u5668\u5df2\u5173\u95ed\u6216\u4e0d\u652f\u6301\u6253\u5370\u3002" + }, + "step": { + "user": { + "description": "\u8bbe\u7f6e Brother \u6253\u5370\u673a\u96c6\u6210\u3002\u5982\u679c\u60a8\u9047\u5230\u95ee\u9898\uff0c\u8bf7\u8bbf\u95ee\uff1ahttps://www.home-assistant.io/integrations/brother" + }, + "zeroconf_confirm": { + "data": { + "type": "\u6253\u5370\u673a\u7c7b\u578b" + }, + "description": "\u60a8\u662f\u5426\u8981\u5c06 Brother \u6253\u5370\u673a {model} (\u5e8f\u5217\u53f7:`{serial_number}`) \u6dfb\u52a0\u5230 Home Assistant ?", + "title": "\u5df2\u53d1\u73b0\u7684 Brother \u6253\u5370\u673a" + } } } } \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/translations/zh-Hans.json b/homeassistant/components/cert_expiry/translations/zh-Hans.json index 07affc990a8..201749ae796 100644 --- a/homeassistant/components/cert_expiry/translations/zh-Hans.json +++ b/homeassistant/components/cert_expiry/translations/zh-Hans.json @@ -1,15 +1,22 @@ { "config": { + "abort": { + "already_configured": "\u670d\u52a1\u5df2\u88ab\u914d\u7f6e", + "import_failed": "\u914d\u7f6e\u5bfc\u5165\u5931\u8d25" + }, "error": { - "connection_timeout": "\u8fde\u63a5\u5230\u6b64\u4e3b\u673a\u65f6\u7684\u8d85\u65f6" + "connection_refused": "\u8fde\u63a5\u5230\u4e3b\u673a\u65f6\u88ab\u62d2\u7edd\u8fde\u63a5", + "connection_timeout": "\u8fde\u63a5\u5230\u6b64\u4e3b\u673a\u65f6\u7684\u8d85\u65f6", + "resolve_failed": "\u65e0\u6cd5\u89e3\u6790\u4e3b\u673a" }, "step": { "user": { "data": { - "host": "\u8bc1\u4e66\u7684\u4e3b\u673a\u540d", + "host": "\u4e3b\u673a\u5730\u5740", "name": "\u8bc1\u4e66\u7684\u540d\u79f0", "port": "\u8bc1\u4e66\u7684\u7aef\u53e3" - } + }, + "title": "\u5b9a\u4e49\u8981\u6d4b\u8bd5\u7684\u8bc1\u4e66" } } } diff --git a/homeassistant/components/co2signal/translations/zh-Hans.json b/homeassistant/components/co2signal/translations/zh-Hans.json new file mode 100644 index 00000000000..af750541de5 --- /dev/null +++ b/homeassistant/components/co2signal/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "\u8bbf\u95ee\u4ee4\u724c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coronavirus/translations/zh-Hans.json b/homeassistant/components/coronavirus/translations/zh-Hans.json index 5bb92ac1172..6348ac40896 100644 --- a/homeassistant/components/coronavirus/translations/zh-Hans.json +++ b/homeassistant/components/coronavirus/translations/zh-Hans.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u6b64\u56fd\u5bb6/\u5730\u533a\u5df2\u914d\u7f6e\u5b8c\u6210\u3002" + "already_configured": "\u6b64\u56fd\u5bb6/\u5730\u533a\u5df2\u914d\u7f6e\u5b8c\u6210\u3002", + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" }, "step": { "user": { diff --git a/homeassistant/components/energy/translations/es.json b/homeassistant/components/energy/translations/es.json new file mode 100644 index 00000000000..64c2f5bffa1 --- /dev/null +++ b/homeassistant/components/energy/translations/es.json @@ -0,0 +1,3 @@ +{ + "title": "Energ\u00eda" +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/zh-Hans.json b/homeassistant/components/ezviz/translations/zh-Hans.json new file mode 100644 index 00000000000..3d8daedec73 --- /dev/null +++ b/homeassistant/components/ezviz/translations/zh-Hans.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "\u670d\u52a1\u5df2\u88ab\u914d\u7f6e", + "ezviz_cloud_account_missing": "\u8424\u77f3\u4e91\u8d26\u53f7\u4e22\u5931\u3002\u8bf7\u91cd\u65b0\u914d\u7f6e\u8424\u77f3\u4e91\u8d26\u53f7\u3002", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u51ed\u8bc1\u65e0\u6548", + "invalid_host": "\u65e0\u6548\u7684\u4e3b\u673a\u5730\u5740\u6216 IP \u5730\u5740" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "\u5bc6\u7801", + "username": "\u7528\u6237\u540d" + }, + "description": "\u8f93\u5165\u5e26\u6709 RTSP \u51ed\u8bc1\u7684\u8424\u77f3\u6444\u50cf\u5934{serial} IP {ip_address} ", + "title": "\u5df2\u53d1\u73b0\u7684\u8424\u77f3\u6444\u50cf\u5934" + }, + "user": { + "data": { + "password": "\u5bc6\u7801", + "url": "URL", + "username": "\u7528\u6237\u540d" + }, + "title": "\u8fde\u63a5\u5230\u8424\u77f3\u4e91" + }, + "user_custom_url": { + "data": { + "password": "\u5bc6\u7801", + "url": "URL", + "username": "\u7528\u6237\u540d" + }, + "description": "\u624b\u52a8\u6307\u5b9a\u4f60\u7684\u533a\u57df\u7f51\u5740", + "title": "\u8fde\u63a5\u5230\u81ea\u5b9a\u4e49\u8424\u77f3\u4e91\u5730\u5740" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "FFmpeg \u53c2\u6570\u4f20\u9012\u81f3\u6444\u50cf\u673a", + "timeout": "\u8bf7\u6c42\u8d85\u65f6\uff08\u79d2\uff09" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/es.json b/homeassistant/components/flipr/translations/es.json new file mode 100644 index 00000000000..478510ba5f1 --- /dev/null +++ b/homeassistant/components/flipr/translations/es.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "unknown": "Error desconocido" + }, + "step": { + "user": { + "data": { + "email": "Correo-e", + "password": "Clave" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/translations/zh-Hans.json b/homeassistant/components/glances/translations/zh-Hans.json index 22cb2995672..a62b5f8b32e 100644 --- a/homeassistant/components/glances/translations/zh-Hans.json +++ b/homeassistant/components/glances/translations/zh-Hans.json @@ -1,15 +1,35 @@ { "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u8fde\u63a5" + }, "error": { - "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "wrong_version": "\u4e0d\u652f\u6301\u7684\u7248\u672c (\u4ec5\u96502\u62163)" }, "step": { "user": { "data": { + "host": "\u4e3b\u673a\u5730\u5740", "name": "\u540d\u79f0", "password": "\u5bc6\u7801", - "username": "\u7528\u6237\u540d" - } + "port": "\u7aef\u53e3", + "ssl": "\u4f7f\u7528 SSL \u51ed\u8bc1", + "username": "\u7528\u6237\u540d", + "verify_ssl": "\u9a8c\u8bc1 SSL \u8bc1\u4e66", + "version": "Glances API \u7248\u672c (2 \u6216 3)" + }, + "title": "\u8bbe\u7f6e Glances" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u9891\u7387" + }, + "description": "\u914d\u7f6e Glances \u9009\u9879" } } } diff --git a/homeassistant/components/gree/translations/zh-Hans.json b/homeassistant/components/gree/translations/zh-Hans.json new file mode 100644 index 00000000000..808f01b57a8 --- /dev/null +++ b/homeassistant/components/gree/translations/zh-Hans.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u6b64\u7f51\u7edc\u672a\u53d1\u73b0\u76f8\u5173\u8bbe\u5907", + "single_instance_allowed": "\u670d\u52a1\u5df2\u88ab\u914d\u7f6e\u3002\u53ea\u5141\u8bb8\u5b58\u5728\u4e00\u4e2a\u914d\u7f6e\u6587\u6863" + }, + "step": { + "confirm": { + "description": "\u4f60\u60f3\u8981\u5f00\u59cb\u914d\u7f6e\u5417\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/es.json b/homeassistant/components/homeassistant/translations/es.json index 562a7335617..0a9342afa69 100644 --- a/homeassistant/components/homeassistant/translations/es.json +++ b/homeassistant/components/homeassistant/translations/es.json @@ -10,6 +10,7 @@ "os_version": "Versi\u00f3n del Sistema Operativo", "python_version": "Versi\u00f3n de Python", "timezone": "Zona horaria", + "user": "Usuario", "version": "Versi\u00f3n", "virtualenv": "Entorno virtual" } diff --git a/homeassistant/components/homeassistant/translations/zh-Hans.json b/homeassistant/components/homeassistant/translations/zh-Hans.json index 617866926b8..e640d502e0c 100644 --- a/homeassistant/components/homeassistant/translations/zh-Hans.json +++ b/homeassistant/components/homeassistant/translations/zh-Hans.json @@ -10,6 +10,7 @@ "os_version": "\u64cd\u4f5c\u7cfb\u7edf\u7248\u672c", "python_version": "Python \u7248\u672c", "timezone": "\u65f6\u533a", + "user": "\u7528\u6237", "version": "\u7248\u672c", "virtualenv": "\u865a\u62df\u73af\u5883" } diff --git a/homeassistant/components/homekit_controller/translations/zh-Hans.json b/homeassistant/components/homekit_controller/translations/zh-Hans.json index 624050e7146..7da392179f6 100644 --- a/homeassistant/components/homekit_controller/translations/zh-Hans.json +++ b/homeassistant/components/homekit_controller/translations/zh-Hans.json @@ -12,6 +12,7 @@ }, "error": { "authentication_error": "HomeKit \u4ee3\u7801\u4e0d\u6b63\u786e\u3002\u8bf7\u68c0\u67e5\u540e\u91cd\u8bd5\u3002", + "insecure_setup_code": "\u8bf7\u6c42\u7684\u8bbe\u7f6e\u4ee3\u7801\u7531\u4e8e\u8fc7\u4e8e\u7b80\u5355\u800c\u4e0d\u5b89\u5168\u3002\u6b64\u914d\u4ef6\u4e0d\u7b26\u5408\u57fa\u672c\u5b89\u5168\u8981\u6c42\u3002", "max_peers_error": "\u8bbe\u5907\u62d2\u7edd\u914d\u5bf9\uff0c\u56e0\u4e3a\u5b83\u6ca1\u6709\u7a7a\u95f2\u7684\u914d\u5bf9\u5b58\u50a8\u7a7a\u95f4\u3002", "pairing_failed": "\u5c1d\u8bd5\u4e0e\u6b64\u8bbe\u5907\u914d\u5bf9\u65f6\u53d1\u751f\u672a\u5904\u7406\u7684\u9519\u8bef\u3002\u8fd9\u53ef\u80fd\u662f\u6682\u65f6\u6027\u6545\u969c\uff0c\u4e5f\u53ef\u80fd\u662f\u60a8\u7684\u8bbe\u5907\u76ee\u524d\u4e0d\u88ab\u652f\u6301\u3002", "unable_to_pair": "\u65e0\u6cd5\u914d\u5bf9\uff0c\u8bf7\u518d\u8bd5\u4e00\u6b21\u3002", @@ -29,6 +30,7 @@ }, "pair": { "data": { + "allow_insecure_setup_codes": "\u5141\u8bb8\u4f7f\u7528\u4e0d\u5b89\u5168\u7684\u8bbe\u7f6e\u4ee3\u7801\u914d\u5bf9\u3002", "pairing_code": "\u914d\u5bf9\u4ee3\u7801" }, "description": "\u8f93\u5165\u60a8\u7684 HomeKit \u914d\u5bf9\u4ee3\u7801\uff08\u683c\u5f0f\u4e3a XXX-XX-XXX\uff09\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6", diff --git a/homeassistant/components/huawei_lte/translations/zh-Hans.json b/homeassistant/components/huawei_lte/translations/zh-Hans.json index 987c53e4d5c..4fb447403d6 100644 --- a/homeassistant/components/huawei_lte/translations/zh-Hans.json +++ b/homeassistant/components/huawei_lte/translations/zh-Hans.json @@ -1,7 +1,8 @@ { "config": { "error": { - "incorrect_username": "\u7528\u6237\u540d\u9519\u8bef" + "incorrect_username": "\u7528\u6237\u540d\u9519\u8bef", + "login_attempts_exceeded": "\u5df2\u8d85\u8fc7\u6700\u5927\u767b\u5f55\u6b21\u6570\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5" } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/zh-Hans.json b/homeassistant/components/ipp/translations/zh-Hans.json index 254f6df9327..38242cae563 100644 --- a/homeassistant/components/ipp/translations/zh-Hans.json +++ b/homeassistant/components/ipp/translations/zh-Hans.json @@ -1,10 +1,25 @@ { "config": { "abort": { - "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "ipp_version_error": "\u6253\u5370\u673a\u4e0d\u652f\u6301\u8be5 IPP \u7248\u672c", + "parse_error": "\u65e0\u6cd5\u89e3\u6790\u6253\u5370\u673a\u54cd\u5e94\u3002" }, "error": { - "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "connection_upgrade": "\u65e0\u6cd5\u8fde\u63a5\u5230\u6253\u5370\u673a\u3002\u8bf7\u9009\u4e2d SSL/TLS \u9009\u9879\u540e\u91cd\u8bd5\u3002" + }, + "step": { + "user": { + "data": { + "base_path": "\u6253\u5370\u673a\u7684\u76f8\u5bf9\u8def\u5f84" + }, + "description": "\u901a\u8fc7 Internet \u6253\u5370\u534f\u8bae (IPP) \u8bbe\u7f6e\u60a8\u7684\u6253\u5370\u673a\uff0c\u4e0e Home Assistant \u8fde\u63a5\u3002", + "title": "\u8fde\u63a5\u60a8\u7684\u6253\u5370\u673a" + }, + "zeroconf_confirm": { + "title": "\u5df2\u53d1\u73b0\u7684\u6253\u5370\u673a" + } } } } \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/zh-Hans.json b/homeassistant/components/kodi/translations/zh-Hans.json index 6fe91b6e995..12915ccdb9b 100644 --- a/homeassistant/components/kodi/translations/zh-Hans.json +++ b/homeassistant/components/kodi/translations/zh-Hans.json @@ -1,11 +1,50 @@ { "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u65e0\u6548\u9a8c\u8bc1", + "no_uuid": "Kodi \u5b9e\u4f8b\u6ca1\u6709\u552f\u4e00\u7684 ID\u3002\u8fd9\u5f88\u53ef\u80fd\u662f\u7531\u4e8e\u65e7\u7684 Kodi \u7248\u672c\uff0817.x \u6216\u66f4\u4f4e\u7248\u672c\uff09\u9020\u6210\u3002\u60a8\u53ef\u4ee5\u624b\u52a8\u914d\u7f6e\u96c6\u6210\u6216\u5347\u7ea7\u5230\u66f4\u65b0\u7684 Kodi \u7248\u672c\u3002", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u9a8c\u8bc1\u65e0\u6548", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "flow_title": "{name}", "step": { "credentials": { "data": { + "password": "\u5bc6\u7801", "username": "\u7528\u6237\u540d" - } + }, + "description": "\u8bf7\u8f93\u5165\u60a8\u7684 Kodi \u7528\u6237\u540d\u548c\u5bc6\u7801\u3002\u8fd9\u4e9b\u53ef\u4ee5\u5728\u201c\u7cfb\u7edf/\u8bbe\u7f6e/\u7f51\u7edc/\u670d\u52a1\u201d\u4e2d\u627e\u5230\u3002" + }, + "discovery_confirm": { + "description": "\u60a8\u662f\u5426\u60f3\u8981\u5c06 Kodi (`{name}`) \u6dfb\u52a0\u5230 Home Assistant?", + "title": "\u5df2\u53d1\u73b0 Kodi" + }, + "user": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "port": "\u7aef\u53e3", + "ssl": "\u4f7f\u7528 SSL \u9a8c\u8bc1" + }, + "description": "Kodi \u8fde\u63a5\u4fe1\u606f\u3002\n\u8bf7\u786e\u4fdd\u5728\u8bbe\u7f6e\uff1a\u201c\u7cfb\u7edf/\u8bbe\u7f6e/\u7f51\u7edc/\u670d\u52a1\u201d\u4e2d\u542f\u7528\u201c\u5141\u8bb8\u901a\u8fc7 HTTP \u63a7\u5236 Kodi\u201d\u3002" + }, + "ws_port": { + "data": { + "ws_port": "\u7aef\u53e3" + }, + "description": "WebSocket \u7aef\u53e3(\u5728 Kodi \u4e2d\u6709\u65f6\u79f0\u4e3a TCP \u7aef\u53e3)\u3002\u4e3a\u901a\u8fc7 WebSocket \u8fdb\u884c\u8fde\u63a5\uff0c\u60a8\u9700\u8981\u5728\"\u7cfb\u7edf/\u8bbe\u7f6e/\u7f51\u7edc/\u670d\u52a1\"\u4e2d\u542f\u7528\u201c\u5141\u8bb8\u7a0b\u5e8f...\u63a7\u5236 Kodi\u201d\u3002\u5982\u679c\u672a\u542f\u7528 WebSocket\uff0c\u8bf7\u79fb\u9664\u7aef\u53e3\u5e76\u7559\u7a7a\u3002" } } + }, + "device_automation": { + "trigger_type": { + "turn_off": "[entity_name} \u88ab\u8981\u6c42\u5173\u95ed", + "turn_on": "[entity_name} \u88ab\u8981\u6c42\u6253\u5f00" + } } } \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/zh-Hans.json b/homeassistant/components/mikrotik/translations/zh-Hans.json index 9604af53495..14916be1264 100644 --- a/homeassistant/components/mikrotik/translations/zh-Hans.json +++ b/homeassistant/components/mikrotik/translations/zh-Hans.json @@ -1,14 +1,33 @@ { "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u9a8c\u8bc1\u65e0\u6548", + "name_exists": "\u540d\u79f0\u5df2\u5b58\u5728" + }, "step": { "user": { "data": { "host": "\u4e3b\u673a", - "name": "\u540d\u5b57", + "name": "\u540d\u79f0", "password": "\u5bc6\u7801", "port": "\u7aef\u53e3", "username": "\u7528\u6237\u540d", - "verify_ssl": "\u4f7f\u7528 ssl" + "verify_ssl": "\u4f7f\u7528 SSL" + }, + "title": "\u8bbe\u7f6e Mikrotik \u8def\u7531\u5668" + } + } + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "\u542f\u7528 ARP Ping", + "force_dhcp": "\u4f7f\u7528 DHCP \u5f3a\u5236\u626b\u63cf" } } } diff --git a/homeassistant/components/minecraft_server/translations/zh-Hans.json b/homeassistant/components/minecraft_server/translations/zh-Hans.json new file mode 100644 index 00000000000..ef3c08c8434 --- /dev/null +++ b/homeassistant/components/minecraft_server/translations/zh-Hans.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52a1\u5df2\u88ab\u914d\u7f6e" + }, + "error": { + "cannot_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230\u670d\u52a1\u5668\u3002\u8bf7\u68c0\u67e5\u4e3b\u673a\u5730\u5740\u548c\u7aef\u53e3\u5e76\u91cd\u8bd5\uff0c\u4e14\u786e\u4fdd\u60a8\u5728\u670d\u52a1\u5668\u4e0a\u8fd0\u884c\u7684 Minecraft \u7248\u672c\u81f3\u5c11\u5728 1.7 \u4ee5\u4e0a\u3002", + "invalid_ip": "IP \u5730\u5740\u65e0\u6548 (\u65e0\u6cd5\u786e\u5b9a MAC \u5730\u5740)\u3002\u8bf7\u66f4\u6b63\u5e76\u91cd\u8bd5\u3002", + "invalid_port": "\u7aef\u53e3\u7684\u8303\u56f4\u5728 1024 \u5230 65535 \u4e4b\u95f4\u3002\u8bf7\u66f4\u6b63\u5e76\u91cd\u8bd5\u3002" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "name": "\u540d\u79f0" + }, + "description": "\u8bbe\u7f6e\u60a8\u7684 Minecraft \u670d\u52a1\u5668\u5b9e\u4f8b\u4ee5\u5141\u8bb8\u76d1\u63a7\u3002", + "title": "\u8fde\u63a5\u60a8\u7684 Minecraft \u670d\u52a1\u5668" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/zh-Hans.json b/homeassistant/components/onvif/translations/zh-Hans.json index 0a0b6db3d38..13dd993228e 100644 --- a/homeassistant/components/onvif/translations/zh-Hans.json +++ b/homeassistant/components/onvif/translations/zh-Hans.json @@ -1,19 +1,69 @@ { "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", + "already_in_progress": "\u914d\u7f6e\u6d41\u5df2\u5728\u8fdb\u884c\u4e2d", + "no_h264": "\u65e0\u53ef\u7528\u7684 H264 \u76f4\u64ad\u6d41\u3002\u8bf7\u68c0\u67e5\u8be5\u8bbe\u5907\u4e0a\u7684\u914d\u7f6e\u6587\u4ef6\u3002", + "no_mac": "\u65e0\u6cd5\u4e3a ONVIF \u914d\u7f6e\u8bbe\u5907\u552f\u4e00 ID", + "onvif_error": "\u914d\u7f6e ONVIF \u8bbe\u5907\u65f6\u51fa\u9519\u3002\u68c0\u67e5\u65e5\u5fd7\u4ee5\u83b7\u53d6\u66f4\u591a\u4fe1\u606f\u3002" + }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25" }, "step": { "auth": { "data": { + "password": "\u5bc6\u7801", "username": "\u7528\u6237\u540d" - } + }, + "title": "\u914d\u7f6e\u8ba4\u8bc1\u4fe1\u606f" + }, + "configure": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "password": "\u5bc6\u7801", + "port": "\u7aef\u53e3", + "username": "\u7528\u6237\u540d" + }, + "title": "\u914d\u7f6e ONVIF \u8bbe\u5907" + }, + "configure_profile": { + "data": { + "include": "\u521b\u5efa\u6444\u50cf\u673a\u5b9e\u4f53" + }, + "description": "\u4ee5 {resolution} \u5206\u8fa8\u7387\u521b\u5efa {profile} \u6444\u50cf\u673a\u5b9e\u4f53\uff1f", + "title": "\u914d\u7f6e \u914d\u7f6e\u6587\u4ef6" + }, + "device": { + "data": { + "host": "\u9009\u62e9\u5df2\u88ab\u53d1\u73b0\u7684 ONVIF \u8bbe\u5907" + }, + "title": "\u9009\u62e9 ONVIF \u8bbe\u5907" + }, + "manual_input": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "name": "\u540d\u79f0", + "port": "\u7aef\u53e3" + }, + "title": "\u914d\u7f6e ONVIF \u8bbe\u5907" + }, + "user": { + "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", + "title": "\u914d\u7f6e ONVIF \u8bbe\u5907" } } }, "options": { "step": { "onvif_devices": { + "data": { + "extra_arguments": "\u9644\u52a0 FFmpeg \u53c2\u6570", + "rtsp_transport": "RTSP \u4f20\u8f93" + }, "title": "ONVIF \u8bbe\u5907\u9009\u9879" } } diff --git a/homeassistant/components/plex/translations/zh-Hans.json b/homeassistant/components/plex/translations/zh-Hans.json index 9cc02584789..02f548f2286 100644 --- a/homeassistant/components/plex/translations/zh-Hans.json +++ b/homeassistant/components/plex/translations/zh-Hans.json @@ -1,11 +1,45 @@ { "config": { + "abort": { + "already_configured": "\u6b64 Plex \u670d\u52a1\u5668\u5df2\u914d\u7f6e", + "already_in_progress": "\u914d\u7f6e\u6d41\u5df2\u5728\u8fdb\u884c\u4e2d", + "reauth_successful": "\u91cd\u9a8c\u8bc1\u6210\u529f", + "token_request_timeout": "\u83b7\u53d6\u4ee4\u724c\u8d85\u65f6", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "error": { + "faulty_credentials": "\u6388\u6743\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u4ee4\u724c\u4fe1\u606f", + "host_or_token": "\u5fc5\u987b\u81f3\u5c11\u63d0\u4f9b\u4e00\u4e2a\u4e3b\u673a\u5730\u5740\u6216\u4ee4\u724c", + "not_found": "\u627e\u4e0d\u5230 Plex \u670d\u52a1\u5668", + "ssl_error": "SSL \u8bc1\u4e66\u9519\u8bef" + }, + "flow_title": "{name} ({host})", "step": { + "manual_setup": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "port": "\u7aef\u53e3", + "ssl": "\u4f7f\u7528 SSL \u8bc1\u4e66", + "token": "\u4ee4\u724c (\u53ef\u9009)", + "verify_ssl": "\u9a8c\u8bc1 SSL \u8bc1\u4e66" + }, + "title": "\u624b\u52a8\u914d\u7f6e" + }, "select_server": { "data": { "server": "\u670d\u52a1\u5668" }, + "description": "\u6709\u591a\u4e2a\u53ef\u7528\u670d\u52a1\u5668\uff0c\u8bf7\u9009\u62e9\uff1a", "title": "\u9009\u62e9 Plex \u670d\u52a1\u5668" + }, + "user": { + "title": "Plex \u5a92\u4f53\u670d\u52a1\u5668" + }, + "user_advanced": { + "data": { + "setup_method": "\u8bbe\u7f6e\u65b9\u6cd5" + }, + "title": "Plex \u5a92\u4f53\u670d\u52a1\u5668" } } }, @@ -14,8 +48,10 @@ "plex_mp_settings": { "data": { "ignore_new_shared_users": "\u5ffd\u7565\u65b0\u589e\u7ba1\u7406/\u5171\u4eab\u4f7f\u7528\u8005", + "ignore_plex_web_clients": "\u5ffd\u7565 Plex Web \u5ba2\u6237\u7aef", "monitored_users": "\u53d7\u76d1\u89c6\u7684\u7528\u6237" - } + }, + "description": "Plex \u5a92\u4f53\u64ad\u653e\u5668\u9009\u9879" } } } diff --git a/homeassistant/components/prosegur/translations/es.json b/homeassistant/components/prosegur/translations/es.json new file mode 100644 index 00000000000..fbccb2f6391 --- /dev/null +++ b/homeassistant/components/prosegur/translations/es.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "El sistema ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n err\u00f3nea", + "unknown": "Error desconocido" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "Vuelva a autenticarse con su cuenta Prosegur.", + "password": "Clave", + "username": "Nombre de Usuario" + } + }, + "user": { + "data": { + "country": "Pa\u00eds", + "password": "Clave", + "username": "Nombre de Usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/pt.json b/homeassistant/components/prosegur/translations/pt.json new file mode 100644 index 00000000000..d479d880d7f --- /dev/null +++ b/homeassistant/components/prosegur/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/es.json b/homeassistant/components/renault/translations/es.json new file mode 100644 index 00000000000..894226d361e --- /dev/null +++ b/homeassistant/components/renault/translations/es.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada" + }, + "error": { + "invalid_credentials": "Autenticaci\u00f3n err\u00f3nea" + }, + "step": { + "user": { + "data": { + "locale": "Configuraci\u00f3n regional", + "password": "Clave", + "username": "Correo-e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/zh-Hans.json b/homeassistant/components/renault/translations/zh-Hans.json new file mode 100644 index 00000000000..b081f64a961 --- /dev/null +++ b/homeassistant/components/renault/translations/zh-Hans.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u6b64\u8d26\u53f7\u5df2\u88ab\u914d\u7f6e", + "kamereon_no_account": "\u65e0\u6cd5\u627e\u5230 Kamereon \u5e10\u6237" + }, + "error": { + "invalid_credentials": "\u65e0\u6548\u8ba4\u8bc1" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereon \u8d26\u53f7 ID" + }, + "title": "\u9009\u62e9 Kamereon \u8d26\u53f7 ID" + }, + "user": { + "data": { + "password": "\u5bc6\u7801", + "username": "\u7535\u5b50\u90ae\u7bb1" + }, + "title": "\u8bbe\u7f6e Renault \u51ed\u8bc1" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/zh-Hans.json b/homeassistant/components/samsungtv/translations/zh-Hans.json new file mode 100644 index 00000000000..da6a5c3c9ba --- /dev/null +++ b/homeassistant/components/samsungtv/translations/zh-Hans.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", + "already_in_progress": "\u914d\u7f6e\u5df2\u5728\u8fdb\u884c\u4e2d", + "auth_missing": "Home Assistant \u672a\u88ab\u5141\u8bb8\u8fde\u63a5\u6b64\u4e09\u661f\u7535\u89c6\u3002\u8bf7\u68c0\u67e5\u60a8\u7684\u7535\u89c6\u8bbe\u7f6e\u3002", + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "id_missing": "\u6b64\u4e09\u661f\u8bbe\u5907\u6ca1\u6709\u5e8f\u5217\u53f7\u3002", + "not_supported": "\u6b64\u4e09\u661f\u8bbe\u5907\u76ee\u524d\u6682\u4e0d\u652f\u6301\u3002", + "reauth_successful": "\u91cd\u9a8c\u8bc1\u6210\u529f", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "error": { + "auth_missing": "Home Assistant \u672a\u88ab\u5141\u8bb8\u8fde\u63a5\u6b64\u4e09\u661f\u7535\u89c6\u3002\u8bf7\u68c0\u67e5\u60a8\u7684\u7535\u89c6\u8bbe\u7f6e\u3002" + }, + "flow_title": "{device}", + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u914d\u7f6e {device} ?\n\u5982\u679c\u60a8\u4e4b\u524d\u4ece\u672a\u8fde\u63a5\u8fc7 Home Assistant \uff0c\u60a8\u5c06\u4f1a\u5728\u8be5\u7535\u89c6\u4e0a\u770b\u5230\u8bf7\u6c42\u6388\u6743\u7684\u5f39\u7a97\u3002", + "title": "\u4e09\u661f\u7535\u89c6" + }, + "reauth_confirm": { + "description": "\u63d0\u4ea4\u4fe1\u606f\u540e\uff0c\u8bf7\u5728 30 \u79d2\u5185\u5728 {device} \u540c\u610f\u83b7\u53d6\u76f8\u5173\u6388\u6743\u3002" + }, + "user": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "name": "\u7528\u6237\u540d" + }, + "description": "\u8f93\u5165\u60a8\u7684\u4e09\u661f\u7535\u89c6\u4fe1\u606f\u3002\u5982\u679c\u60a8\u4e4b\u524d\u4ece\u672a\u8fde\u63a5\u8fc7 Home Assistant \uff0c\u60a8\u5c06\u4f1a\u5728\u8be5\u7535\u89c6\u4e0a\u770b\u5230\u8bf7\u6c42\u6388\u6743\u7684\u5f39\u7a97\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/translations/zh-Hans.json b/homeassistant/components/solaredge/translations/zh-Hans.json index baf8c980cb7..7f5039e9f93 100644 --- a/homeassistant/components/solaredge/translations/zh-Hans.json +++ b/homeassistant/components/solaredge/translations/zh-Hans.json @@ -1,10 +1,22 @@ { "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e" + }, + "error": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", + "could_not_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230 SolarEdge API", + "invalid_api_key": "\u65e0\u6548\u7684 API \u5bc6\u94a5", + "site_not_active": "\u672a\u6fc0\u6d3b" + }, "step": { "user": { "data": { - "api_key": "API \u5bc6\u7801" - } + "api_key": "API \u5bc6\u7801", + "name": "\u5b89\u88c5\u540d\u79f0", + "site_id": "SolarEdge \u7ad9\u70b9 ID" + }, + "title": "\u5b9a\u4e49\u672c\u6b21\u5b89\u88c5\u7684 API \u53c2\u6570" } } } diff --git a/homeassistant/components/spotify/translations/zh-Hans.json b/homeassistant/components/spotify/translations/zh-Hans.json index 19a6909de48..fdda1685cf1 100644 --- a/homeassistant/components/spotify/translations/zh-Hans.json +++ b/homeassistant/components/spotify/translations/zh-Hans.json @@ -1,4 +1,23 @@ { + "config": { + "abort": { + "authorize_url_timeout": "\u751f\u6210\u6388\u6743\u7f51\u5740\u8d85\u65f6\u3002", + "missing_configuration": "Spotify \u96c6\u6210\u672a\u914d\u7f6e \u3002\u8bf7\u9075\u5faa\u6587\u6863\u914d\u7f6e\u3002", + "no_url_available": "\u65e0 URL \u53ef\u7528\uff0c\u66f4\u591a\u4fe1\u606f\u8bf7[check the help section]({docs_url})", + "reauth_account_mismatch": "\u5df2\u9a8c\u8bc1\u7684 Spotify \u5e10\u6237\u4e0e\u9700\u8981\u91cd\u65b0\u9a8c\u8bc1\u7684\u5e10\u6237\u4e0d\u5339\u914d\u3002" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u901a\u8fc7 Spotify \u8fdb\u884c\u8eab\u4efd\u9a8c\u8bc1\u3002" + }, + "step": { + "pick_implementation": { + "title": "\u9009\u62e9\u9a8c\u8bc1\u65b9\u5f0f" + }, + "reauth_confirm": { + "description": "Spotify \u96c6\u6210\u9700\u8981\u91cd\u65b0\u9a8c\u8bc1\u5e10\u6237\uff1a {account}" + } + } + }, "system_health": { "info": { "api_endpoint_reachable": "\u53ef\u8bbf\u95ee Spotify API" diff --git a/homeassistant/components/syncthing/translations/zh-Hans.json b/homeassistant/components/syncthing/translations/zh-Hans.json new file mode 100644 index 00000000000..87d3db5c83f --- /dev/null +++ b/homeassistant/components/syncthing/translations/zh-Hans.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52a1\u5df2\u88ab\u914d\u7f6e" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u8ba4\u8bc1\u65e0\u6548" + }, + "step": { + "user": { + "data": { + "title": "\u8bbe\u7f6e Syncthing \u96c6\u6210", + "token": "\u4ee4\u724c", + "url": "\u8fde\u63a5\u5730\u5740", + "verify_ssl": "\u9a8c\u8bc1 SSL \u8bc1\u4e66" + } + } + } + }, + "title": "Syncthing" +} \ No newline at end of file diff --git a/homeassistant/components/syncthru/translations/zh-Hans.json b/homeassistant/components/syncthru/translations/zh-Hans.json new file mode 100644 index 00000000000..c50e250aee9 --- /dev/null +++ b/homeassistant/components/syncthru/translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown_state": "\u6253\u5370\u673a\u72b6\u6001\u672a\u77e5\uff0c\u8bf7\u9a8c\u8bc1 URL \u548c\u7f51\u7edc\u662f\u5426\u8fde\u63a5\u6b63\u5e38" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/zh-Hans.json b/homeassistant/components/synology_dsm/translations/zh-Hans.json index b4edf8039a6..862f526c38d 100644 --- a/homeassistant/components/synology_dsm/translations/zh-Hans.json +++ b/homeassistant/components/synology_dsm/translations/zh-Hans.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86" + "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86", + "reauth_successful": "\u91cd\u9a8c\u8bc1" }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", @@ -28,13 +29,20 @@ "description": "\u60a8\u60f3\u8981\u914d\u7f6e {name} ({host}) \u5417\uff1f", "title": "Synology DSM" }, + "reauth": { + "data": { + "password": "\u5bc6\u7801", + "username": "\u7528\u6237\u540d" + } + }, "user": { "data": { "host": "\u4e3b\u673a", "password": "\u5bc6\u7801", "port": "\u7aef\u53e3", "ssl": "\u4f7f\u7528 SSL \u8bc1\u4e66", - "username": "\u7528\u6237\u540d" + "username": "\u7528\u6237\u540d", + "verify_ssl": "\u9a8c\u8bc1 SSL \u8bc1\u4e66" }, "title": "Synology DSM" } diff --git a/homeassistant/components/tractive/translations/es.json b/homeassistant/components/tractive/translations/es.json new file mode 100644 index 00000000000..11aa4f1aa9c --- /dev/null +++ b/homeassistant/components/tractive/translations/es.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El sistema ya est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n err\u00f3nea", + "unknown": "Error desconocido" + }, + "step": { + "user": { + "data": { + "email": "Correo-e", + "password": "Clave" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/he.json b/homeassistant/components/tractive/translations/he.json new file mode 100644 index 00000000000..1cccac175a0 --- /dev/null +++ b/homeassistant/components/tractive/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": { + "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/tractive/translations/pt.json b/homeassistant/components/tractive/translations/pt.json new file mode 100644 index 00000000000..7430480cc09 --- /dev/null +++ b/homeassistant/components/tractive/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/zh-Hans.json b/homeassistant/components/tractive/translations/zh-Hans.json new file mode 100644 index 00000000000..5d8e6c66984 --- /dev/null +++ b/homeassistant/components/tractive/translations/zh-Hans.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u5b58\u5728\u914d\u7f6e\u6587\u6863" + }, + "error": { + "invalid_auth": "\u8ba4\u8bc1\u65e0\u6548", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "step": { + "user": { + "data": { + "email": "\u7535\u5b50\u90ae\u7bb1", + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/zh-Hans.json b/homeassistant/components/transmission/translations/zh-Hans.json index d217ccdc842..a056b99a4bb 100644 --- a/homeassistant/components/transmission/translations/zh-Hans.json +++ b/homeassistant/components/transmission/translations/zh-Hans.json @@ -1,10 +1,34 @@ { "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u9a8c\u8bc1\u65e0\u6548", + "name_exists": "\u540d\u79f0\u5df2\u5b58\u5728" + }, "step": { "user": { "data": { - "password": "\u5bc6\u7801" - } + "host": "\u4e3b\u673a\u5730\u5740", + "name": "\u540d\u79f0", + "password": "\u5bc6\u7801", + "port": "\u7aef\u53e3", + "username": "\u7528\u6237\u540d" + }, + "title": "\u914d\u7f6e Transmission \u5ba2\u6237\u7aef" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "limit": "\u9650\u5236", + "scan_interval": "\u66f4\u65b0\u9891\u7387" + }, + "title": "Transmission \u914d\u7f6e\u9009\u9879" } } } diff --git a/homeassistant/components/uptimerobot/translations/es.json b/homeassistant/components/uptimerobot/translations/es.json new file mode 100644 index 00000000000..1f88050745d --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada", + "unknown": "Error desconocido" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_api_key": "Clave de la API err\u00f3nea", + "unknown": "Error desconocido" + }, + "step": { + "user": { + "data": { + "api_key": "Clave de la API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/he.json b/homeassistant/components/uptimerobot/translations/he.json index 5b6fc485e04..1a45e5c78cd 100644 --- a/homeassistant/components/uptimerobot/translations/he.json +++ b/homeassistant/components/uptimerobot/translations/he.json @@ -1,10 +1,12 @@ { "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", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { diff --git a/homeassistant/components/uptimerobot/translations/pt.json b/homeassistant/components/uptimerobot/translations/pt.json new file mode 100644 index 00000000000..10c16aafa0f --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "unknown": "Erro inesperado" + }, + "error": { + "invalid_api_key": "Chave de API inv\u00e1lida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/zh-Hans.json b/homeassistant/components/uptimerobot/translations/zh-Hans.json new file mode 100644 index 00000000000..92106b06ce2 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/zh-Hans.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u6b64\u8d26\u53f7\u5df2\u88ab\u914d\u7f6e", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_api_key": "\u65e0\u6548\u7684 API \u5bc6\u94a5", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u94a5" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/zh-Hans.json b/homeassistant/components/vizio/translations/zh-Hans.json new file mode 100644 index 00000000000..1fa1ebc751d --- /dev/null +++ b/homeassistant/components/vizio/translations/zh-Hans.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "pair_tv": { + "title": "\u5b8c\u6210\u914d\u5bf9\u8fc7\u7a0b" + }, + "pairing_complete": { + "title": "\u914d\u5bf9\u5b8c\u6210" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.es.json b/homeassistant/components/xiaomi_miio/translations/select.es.json new file mode 100644 index 00000000000..3906ef91342 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.es.json @@ -0,0 +1,9 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "bright": "Brillo", + "dim": "Atenuar", + "off": "Apagado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.zh-Hans.json b/homeassistant/components/xiaomi_miio/translations/select.zh-Hans.json new file mode 100644 index 00000000000..bad6ba91597 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.zh-Hans.json @@ -0,0 +1,9 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "bright": "\u4eae", + "dim": "\u6697", + "off": "\u5173" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/es.json b/homeassistant/components/yale_smart_alarm/translations/es.json new file mode 100644 index 00000000000..b970badb079 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/es.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n err\u00f3nea" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "ID de \u00c1rea", + "name": "Nombre", + "password": "Clave", + "username": "Nombre de usuario" + } + }, + "user": { + "data": { + "area_id": "ID de \u00e1rea", + "name": "Nombre", + "password": "Clave", + "username": "Nombre de usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/es.json b/homeassistant/components/youless/translations/es.json new file mode 100644 index 00000000000..72a56cc5608 --- /dev/null +++ b/homeassistant/components/youless/translations/es.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "host": "Anfitri\u00f3n", + "name": "Nombre" + } + } + } + } +} \ No newline at end of file From b9e0de2eed63848d1c6370efeb8bab0765e62515 Mon Sep 17 00:00:00 2001 From: Trinnik Date: Sat, 7 Aug 2021 21:51:05 -0600 Subject: [PATCH 032/355] Fix update entity prior to adding (#54015) --- homeassistant/components/aladdin_connect/cover.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 85f89f3043b..14e2b2f0ce2 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -49,7 +49,10 @@ def setup_platform( try: if not acc.login(): raise ValueError("Username or Password is incorrect") - add_entities(AladdinDevice(acc, door) for door in acc.get_doors()) + add_entities( + (AladdinDevice(acc, door) for door in acc.get_doors()), + update_before_add = True + ) except (TypeError, KeyError, NameError, ValueError) as ex: _LOGGER.error("%s", ex) hass.components.persistent_notification.create( From 22acaa8e63aa09dd5403d6f6d2bc17ba1b17d453 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 7 Aug 2021 21:00:37 -0700 Subject: [PATCH 033/355] Pin google-cloud-pubsub to an older version (#54239) Pin google-cloud-pubsub to an older version, since newer versions have a pin that is incompatible with the existing grpcio pin already in package_constraints.txt --- homeassistant/package_constraints.txt | 5 +++++ script/gen_requirements_all.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 617a057743b..6d22aa51b24 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -51,6 +51,11 @@ httplib2>=0.19.0 # https://github.com/home-assistant/core/issues/40148 grpcio==1.31.0 +# Newer versions of cloud pubsub pin a higher version of grpcio. This can +# be reverted when the grpcio pin is reverted, see: +# https://github.com/home-assistant/core/issues/53427 +google-cloud-pubsub==2.1.0 + # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 7dcc4f71fe8..934ea9be90c 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -73,6 +73,11 @@ httplib2>=0.19.0 # https://github.com/home-assistant/core/issues/40148 grpcio==1.31.0 +# Newer versions of cloud pubsub pin a higher version of grpcio. This can +# be reverted when the grpcio pin is reverted, see: +# https://github.com/home-assistant/core/issues/53427 +google-cloud-pubsub==2.1.0 + # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 From d3007c26b31fab0387452371b700d49db640ec89 Mon Sep 17 00:00:00 2001 From: carstenschroeder Date: Sun, 8 Aug 2021 06:03:20 +0200 Subject: [PATCH 034/355] Bugfix: Bring back unique IDs for ADS covers after #52488 (#54212) --- homeassistant/components/ads/cover.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ads/cover.py b/homeassistant/components/ads/cover.py index 0cd0264cb50..976bfd58fed 100644 --- a/homeassistant/components/ads/cover.py +++ b/homeassistant/components/ads/cover.py @@ -91,13 +91,13 @@ class AdsCover(AdsEntity, CoverEntity): ): """Initialize AdsCover entity.""" super().__init__(ads_hub, name, ads_var_is_closed) - if self._ads_var is None: + if self._attr_unique_id is None: if ads_var_position is not None: - self._unique_id = ads_var_position + self._attr_unique_id = ads_var_position elif ads_var_pos_set is not None: - self._unique_id = ads_var_pos_set + self._attr_unique_id = ads_var_pos_set elif ads_var_open is not None: - self._unique_id = ads_var_open + self._attr_unique_id = ads_var_open self._state_dict[STATE_KEY_POSITION] = None self._ads_var_position = ads_var_position From 2232915ea830071f9039b221f286ded7fc60096a Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 8 Aug 2021 06:10:08 +0200 Subject: [PATCH 035/355] Add parameter to delay sending of requests in modbus (#54203) --- homeassistant/components/modbus/__init__.py | 2 ++ homeassistant/components/modbus/const.py | 1 + homeassistant/components/modbus/modbus.py | 11 +++++++++-- tests/components/modbus/test_init.py | 2 ++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 16be39230db..43aa49e6da7 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -66,6 +66,7 @@ from .const import ( CONF_INPUT_TYPE, CONF_MAX_TEMP, CONF_MIN_TEMP, + CONF_MSG_WAIT, CONF_PARITY, CONF_PRECISION, CONF_RETRIES, @@ -283,6 +284,7 @@ MODBUS_SCHEMA = vol.Schema( vol.Optional(CONF_DELAY, default=0): cv.positive_int, vol.Optional(CONF_RETRIES, default=3): cv.positive_int, vol.Optional(CONF_RETRY_ON_EMPTY, default=False): cv.boolean, + vol.Optional(CONF_MSG_WAIT): cv.positive_int, vol.Optional(CONF_BINARY_SENSORS): vol.All( cv.ensure_list, [BINARY_SENSOR_SCHEMA] ), diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 10eb07f801e..ef6d7c3fc32 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -30,6 +30,7 @@ CONF_INPUTS = "inputs" CONF_INPUT_TYPE = "input_type" CONF_MAX_TEMP = "max_temp" CONF_MIN_TEMP = "min_temp" +CONF_MSG_WAIT = "message_wait_milliseconds" CONF_PARITY = "parity" CONF_REGISTER = "register" CONF_REGISTER_TYPE = "register_type" diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 77d8b669c24..dad91f26a12 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -39,6 +39,7 @@ from .const import ( CONF_BAUDRATE, CONF_BYTESIZE, CONF_CLOSE_COMM_ON_ERROR, + CONF_MSG_WAIT, CONF_PARITY, CONF_RETRIES, CONF_RETRY_ON_EMPTY, @@ -227,6 +228,12 @@ class ModbusHub: self._pb_params["framer"] = ModbusRtuFramer Defaults.Timeout = client_config[CONF_TIMEOUT] + if CONF_MSG_WAIT in client_config: + self._msg_wait = client_config[CONF_MSG_WAIT] / 1000 + elif self._config_type == CONF_SERIAL: + self._msg_wait = 30 / 1000 + else: + self._msg_wait = 0 def _log_error(self, text: str, error_state=True): log_text = f"Pymodbus: {text}" @@ -322,7 +329,7 @@ class ModbusHub: result = await self.hass.async_add_executor_job( self._pymodbus_call, unit, address, value, use_call ) - if self._config_type == "serial": + if self._msg_wait: # small delay until next request/response - await asyncio.sleep(30 / 1000) + await asyncio.sleep(self._msg_wait) return result diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 8b8d063bf02..b9f6420604f 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -40,6 +40,7 @@ from homeassistant.components.modbus.const import ( CONF_BYTESIZE, CONF_DATA_TYPE, CONF_INPUT_TYPE, + CONF_MSG_WAIT, CONF_PARITY, CONF_STOPBITS, CONF_SWAP, @@ -245,6 +246,7 @@ async def test_exception_struct_validator(do_config): CONF_PORT: "usb01", CONF_PARITY: "E", CONF_STOPBITS: 1, + CONF_MSG_WAIT: 100, }, { CONF_TYPE: "serial", From a001fd5000e59f6fda8885600d034c0fe3c3c3cf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 7 Aug 2021 21:10:21 -0700 Subject: [PATCH 036/355] Fix formatting (#54247) --- homeassistant/components/aladdin_connect/cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 14e2b2f0ce2..5cebe3622dc 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -51,7 +51,7 @@ def setup_platform( raise ValueError("Username or Password is incorrect") add_entities( (AladdinDevice(acc, door) for door in acc.get_doors()), - update_before_add = True + update_before_add=True, ) except (TypeError, KeyError, NameError, ValueError) as ex: _LOGGER.error("%s", ex) From 8a4674c086d3434833db55bf9a9907ff778efd68 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 8 Aug 2021 06:11:56 +0200 Subject: [PATCH 037/355] Solve missing automatic update of struct configuration in modbus (#54193) --- homeassistant/components/modbus/validators.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 3efb61f8027..b59557e58d2 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -86,6 +86,7 @@ def struct_validator(config): _LOGGER.warning(error) try: data_type = OLD_DATA_TYPES[data_type][config.get(CONF_COUNT, 1)] + config[CONF_DATA_TYPE] = data_type except KeyError as exp: error = f"{name} cannot convert automatically {data_type}" raise vol.Invalid(error) from exp From 53c64e5148a47b14cfb4cde4d852a950118037b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 8 Aug 2021 06:12:55 +0200 Subject: [PATCH 038/355] Handle added and removed monitors (#54228) --- .../components/uptimerobot/__init__.py | 82 +++++++++++++++---- 1 file changed, 65 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/uptimerobot/__init__.py b/homeassistant/components/uptimerobot/__init__.py index 17bc8f9a629..07782fda533 100644 --- a/homeassistant/components/uptimerobot/__init__.py +++ b/homeassistant/components/uptimerobot/__init__.py @@ -7,6 +7,7 @@ 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.device_registry import DeviceRegistry, async_get_registry from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import API_ATTR_OK, COORDINATOR_UPDATE_INTERVAL, DOMAIN, LOGGER, PLATFORMS @@ -18,25 +19,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: uptime_robot_api = UptimeRobot( entry.data[CONF_API_KEY], async_get_clientsession(hass) ) + dev_reg = await async_get_registry(hass) - async def async_update_data() -> list[UptimeRobotMonitor]: - """Fetch data from API UptimeRobot API.""" - try: - response = await uptime_robot_api.async_get_monitors() - except UptimeRobotException as exception: - raise UpdateFailed(exception) from exception - else: - if response.status == API_ATTR_OK: - monitors: list[UptimeRobotMonitor] = response.data - return monitors - raise UpdateFailed(response.error.message) - - hass.data[DOMAIN][entry.entry_id] = coordinator = DataUpdateCoordinator( + hass.data[DOMAIN][entry.entry_id] = coordinator = UptimeRobotDataUpdateCoordinator( hass, - LOGGER, - name=DOMAIN, - update_method=async_update_data, - update_interval=COORDINATOR_UPDATE_INTERVAL, + config_entry_id=entry.entry_id, + dev_reg=dev_reg, + api=uptime_robot_api, ) await coordinator.async_config_entry_first_refresh() @@ -53,3 +42,62 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +class UptimeRobotDataUpdateCoordinator(DataUpdateCoordinator): + """Data update coordinator for Uptime Robot.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry_id: str, + dev_reg: DeviceRegistry, + api: UptimeRobot, + ) -> None: + """Initialize coordinator.""" + super().__init__( + hass, + LOGGER, + name=DOMAIN, + update_method=self._async_update_data, + update_interval=COORDINATOR_UPDATE_INTERVAL, + ) + self._config_entry_id = config_entry_id + self._device_registry = dev_reg + self._api = api + + async def _async_update_data(self) -> list[UptimeRobotMonitor] | None: + """Update data.""" + try: + response = await self._api.async_get_monitors() + except UptimeRobotException as exception: + raise UpdateFailed(exception) from exception + else: + if response.status != API_ATTR_OK: + raise UpdateFailed(response.error.message) + + monitors: list[UptimeRobotMonitor] = response.data + + current_monitors = { + list(device.identifiers)[0][1] + for device in self._device_registry.devices.values() + if self._config_entry_id in device.config_entries + and list(device.identifiers)[0][0] == DOMAIN + } + new_monitors = {str(monitor.id) for monitor in monitors} + if stale_monitors := current_monitors - new_monitors: + for monitor_id in stale_monitors: + if device := self._device_registry.async_get_device( + {(DOMAIN, monitor_id)} + ): + self._device_registry.async_remove_device(device.id) + + # If there are new monitors, we should reload the config entry so we can + # create new devices and entities. + if self.data and new_monitors - {str(monitor.id) for monitor in self.data}: + self.hass.async_create_task( + self.hass.config_entries.async_reload(self._config_entry_id) + ) + return None + + return monitors From 11f15f66afbf8968c3020a53f11926773c374968 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 8 Aug 2021 05:20:55 +0100 Subject: [PATCH 039/355] OVO Energy Long-term Statistics (#54157) Co-authored-by: Paulus Schoutsen --- homeassistant/components/ovo_energy/sensor.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index 7615a7011d3..f678caf02b0 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -1,4 +1,6 @@ """Support for OVO Energy sensors.""" +from __future__ import annotations + from datetime import timedelta from ovoenergy import OVODailyUsage @@ -6,8 +8,10 @@ from ovoenergy.ovoenergy import OVOEnergy from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_CLASS_ENERGY, DEVICE_CLASS_MONETARY from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util.dt import utc_from_timestamp from . import OVOEnergyDeviceEntity from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN @@ -57,6 +61,9 @@ async def async_setup_entry( class OVOEnergySensor(OVOEnergyDeviceEntity, SensorEntity): """Defines a OVO Energy sensor.""" + _attr_last_reset = utc_from_timestamp(0) + _attr_state_class = "measurement" + def __init__( self, coordinator: DataUpdateCoordinator, @@ -64,15 +71,17 @@ class OVOEnergySensor(OVOEnergyDeviceEntity, SensorEntity): key: str, name: str, icon: str, - unit_of_measurement: str = "", + device_class: str | None, + unit_of_measurement: str | None, ) -> None: """Initialize OVO Energy sensor.""" + self._attr_device_class = device_class self._unit_of_measurement = unit_of_measurement super().__init__(coordinator, client, key, name, icon) @property - def unit_of_measurement(self) -> str: + def unit_of_measurement(self) -> str | None: """Return the unit this state is expressed in.""" return self._unit_of_measurement @@ -89,6 +98,7 @@ class OVOEnergyLastElectricityReading(OVOEnergySensor): f"{client.account_id}_last_electricity_reading", "OVO Last Electricity Reading", "mdi:flash", + DEVICE_CLASS_ENERGY, "kWh", ) @@ -124,6 +134,7 @@ class OVOEnergyLastGasReading(OVOEnergySensor): f"{DOMAIN}_{client.account_id}_last_gas_reading", "OVO Last Gas Reading", "mdi:gas-cylinder", + DEVICE_CLASS_ENERGY, "kWh", ) @@ -160,6 +171,7 @@ class OVOEnergyLastElectricityCost(OVOEnergySensor): f"{DOMAIN}_{client.account_id}_last_electricity_cost", "OVO Last Electricity Cost", "mdi:cash-multiple", + DEVICE_CLASS_MONETARY, currency, ) @@ -196,6 +208,7 @@ class OVOEnergyLastGasCost(OVOEnergySensor): f"{DOMAIN}_{client.account_id}_last_gas_cost", "OVO Last Gas Cost", "mdi:cash-multiple", + DEVICE_CLASS_MONETARY, currency, ) From 75726a26954cf2527c9fa28b9de2200232d255d5 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 7 Aug 2021 21:29:52 -0700 Subject: [PATCH 040/355] Don't block motionEye setup on NoURLAvailableError (#54225) Co-authored-by: Paulus Schoutsen --- .../components/motioneye/__init__.py | 68 +++++++++++-------- tests/components/motioneye/test_web_hooks.py | 31 +++++++++ 2 files changed, 71 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 2ade7c48e1b..acafdceeb05 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -53,7 +53,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity import DeviceInfo, EntityDescription -from homeassistant.helpers.network import get_url +from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -145,12 +145,21 @@ def listen_for_new_cameras( @callback -def async_generate_motioneye_webhook(hass: HomeAssistant, webhook_id: str) -> str: +def async_generate_motioneye_webhook( + hass: HomeAssistant, webhook_id: str +) -> str | None: """Generate the full local URL for a webhook_id.""" - return "{}{}".format( - get_url(hass, allow_cloud=False), - async_generate_path(webhook_id), - ) + try: + return "{}{}".format( + get_url(hass, allow_cloud=False), + async_generate_path(webhook_id), + ) + except NoURLAvailableError: + _LOGGER.warning( + "Unable to get Home Assistant URL. Have you set the internal and/or " + "external URLs in Configuration -> General?" + ) + return None @callback @@ -228,28 +237,31 @@ def _add_camera( if entry.options.get(CONF_WEBHOOK_SET, DEFAULT_WEBHOOK_SET): url = async_generate_motioneye_webhook(hass, entry.data[CONF_WEBHOOK_ID]) - if _set_webhook( - _build_url( - device, - url, - EVENT_MOTION_DETECTED, - EVENT_MOTION_DETECTED_KEYS, - ), - KEY_WEB_HOOK_NOTIFICATIONS_URL, - KEY_WEB_HOOK_NOTIFICATIONS_HTTP_METHOD, - KEY_WEB_HOOK_NOTIFICATIONS_ENABLED, - camera, - ) | _set_webhook( - _build_url( - device, - url, - EVENT_FILE_STORED, - EVENT_FILE_STORED_KEYS, - ), - KEY_WEB_HOOK_STORAGE_URL, - KEY_WEB_HOOK_STORAGE_HTTP_METHOD, - KEY_WEB_HOOK_STORAGE_ENABLED, - camera, + if url and ( + _set_webhook( + _build_url( + device, + url, + EVENT_MOTION_DETECTED, + EVENT_MOTION_DETECTED_KEYS, + ), + KEY_WEB_HOOK_NOTIFICATIONS_URL, + KEY_WEB_HOOK_NOTIFICATIONS_HTTP_METHOD, + KEY_WEB_HOOK_NOTIFICATIONS_ENABLED, + camera, + ) + | _set_webhook( + _build_url( + device, + url, + EVENT_FILE_STORED, + EVENT_FILE_STORED_KEYS, + ), + KEY_WEB_HOOK_STORAGE_URL, + KEY_WEB_HOOK_STORAGE_HTTP_METHOD, + KEY_WEB_HOOK_STORAGE_ENABLED, + camera, + ) ): hass.async_create_task(client.async_set_camera(camera_id, camera)) diff --git a/tests/components/motioneye/test_web_hooks.py b/tests/components/motioneye/test_web_hooks.py index 03b4e8bc46a..f20ef5101e9 100644 --- a/tests/components/motioneye/test_web_hooks.py +++ b/tests/components/motioneye/test_web_hooks.py @@ -32,11 +32,13 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.network import NoURLAvailableError from homeassistant.setup import async_setup_component from . import ( TEST_CAMERA, TEST_CAMERA_DEVICE_IDENTIFIER, + TEST_CAMERA_ENTITY_ID, TEST_CAMERA_ID, TEST_CAMERA_NAME, TEST_CAMERAS, @@ -251,6 +253,35 @@ async def test_setup_camera_with_correct_webhook( assert not client.async_set_camera.called +async def test_setup_camera_with_no_home_assistant_urls( + hass: HomeAssistant, + caplog: Any, +) -> None: + """Verify setup works without Home Assistant internal/external URLs.""" + + client = create_mock_motioneye_client() + config_entry = create_mock_motioneye_config_entry(hass, data={CONF_URL: TEST_URL}) + + with patch( + "homeassistant.components.motioneye.get_url", side_effect=NoURLAvailableError + ): + await setup_mock_motioneye_config_entry( + hass, + config_entry=config_entry, + client=client, + ) + + # Should log a warning ... + assert "Unable to get Home Assistant URL" in caplog.text + + # ... should not set callbacks in the camera ... + assert not client.async_set_camera.called + + # ... but camera should still be present. + entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID) + assert entity_state + + async def test_good_query(hass: HomeAssistant, aiohttp_client: Any) -> None: """Test good callbacks.""" await async_setup_component(hass, "http", {"http": {}}) From 7d29eb282bf1670692d6bc4d38d76dec22029279 Mon Sep 17 00:00:00 2001 From: rjulius23 Date: Sun, 8 Aug 2021 07:02:20 +0200 Subject: [PATCH 041/355] Add enumerate to builtins in python_script component (#54244) --- homeassistant/components/python_script/__init__.py | 1 + tests/components/python_script/test_init.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index 89a7ab4ba04..922f5b71a3c 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -195,6 +195,7 @@ def execute(hass, filename, source, data=None): "sum": sum, "any": any, "all": all, + "enumerate": enumerate, } builtins = safe_builtins.copy() builtins.update(utility_builtins) diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index 142d833698d..1e1f24b6eee 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -179,6 +179,20 @@ for i in [1, 2]: assert hass.states.is_state("hello.2", "world") +async def test_using_enumerate(hass): + """Test that enumerate is accepted and executed.""" + source = """ +for index, value in enumerate(["earth", "mars"]): + hass.states.set('hello.{}'.format(index), value) + """ + + hass.async_add_job(execute, hass, "test.py", source, {}) + await hass.async_block_till_done() + + assert hass.states.is_state("hello.0", "earth") + assert hass.states.is_state("hello.1", "mars") + + async def test_unpacking_sequence(hass, caplog): """Test compile error logs error.""" caplog.set_level(logging.ERROR) From fc40735295740f5b43e2e686cd2664e4ee17dc80 Mon Sep 17 00:00:00 2001 From: Schmidsfeld <68500293+Schmidsfeld@users.noreply.github.com> Date: Sun, 8 Aug 2021 11:23:28 +0200 Subject: [PATCH 042/355] Add more Fritz sensors for DSL connections (#53198) * Update sensor.py Added information about the upstream line accorrding to fritzconnection library (available since V1.5.0) . New information available are line sync speed,, noise margin and power attenuation. Tested with ADSL and VDSL lines on fritzbox 7590, 7490 and 7390. Not tested on cable internet / fiber. According to upstrem library should also work / fail gracefully. * Update sensor.py Fixed errors from automated tests Sorry it took so long * Update homeassistant/components/fritz/sensor.py Thank you this sounds even better Co-authored-by: Simone Chemelli * Update homeassistant/components/fritz/sensor.py Co-authored-by: Simone Chemelli * Update homeassistant/components/fritz/sensor.py Co-authored-by: Simone Chemelli * Update homeassistant/components/fritz/sensor.py Co-authored-by: Simone Chemelli * Update homeassistant/components/fritz/sensor.py Co-authored-by: Simone Chemelli * Update homeassistant/components/fritz/sensor.py Co-authored-by: Simone Chemelli * Update homeassistant/components/fritz/sensor.py Co-authored-by: Simone Chemelli * Update homeassistant/components/fritz/sensor.py Co-authored-by: Simone Chemelli * Update homeassistant/components/fritz/sensor.py Co-authored-by: Simone Chemelli * black & mypy fixes * Rebase, fix multiplier, add conditional create Co-authored-by: Simone Chemelli --- homeassistant/components/fritz/const.py | 2 + homeassistant/components/fritz/sensor.py | 103 +++++++++++++++++++++-- 2 files changed, 99 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index 8b3f9106602..4ae8314113f 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -6,6 +6,8 @@ PLATFORMS = ["binary_sensor", "device_tracker", "sensor", "switch"] DATA_FRITZ = "fritz_data" +DSL_CONNECTION = "dsl" + DEFAULT_DEVICE_NAME = "Unknown device" DEFAULT_HOST = "192.168.178.1" DEFAULT_PORT = 49000 diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index d7a34564b43..c7d3fc243a5 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -15,13 +15,14 @@ from homeassistant.const import ( DATA_RATE_KILOBITS_PER_SECOND, DATA_RATE_KILOBYTES_PER_SECOND, DEVICE_CLASS_TIMESTAMP, + SIGNAL_STRENGTH_DECIBELS, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow from .common import FritzBoxBaseEntity, FritzBoxTools -from .const import DOMAIN, UPTIME_DEVIATION +from .const import DOMAIN, DSL_CONNECTION, UPTIME_DEVIATION _LOGGER = logging.getLogger(__name__) @@ -89,6 +90,44 @@ def _retrieve_gb_received_state(status: FritzStatus, last_value: str) -> float: return round(status.bytes_received / 1000 / 1000 / 1000, 1) # type: ignore[no-any-return] +def _retrieve_link_kb_s_sent_state(status: FritzStatus, last_value: str) -> float: + """Return upload link rate.""" + return round(status.max_linked_bit_rate[0] / 1000, 1) # type: ignore[no-any-return] + + +def _retrieve_link_kb_s_received_state(status: FritzStatus, last_value: str) -> float: + """Return download link rate.""" + return round(status.max_linked_bit_rate[1] / 1000, 1) # type: ignore[no-any-return] + + +def _retrieve_link_noise_margin_sent_state( + status: FritzStatus, last_value: str +) -> float: + """Return upload noise margin.""" + return status.noise_margin[0] # type: ignore[no-any-return] + + +def _retrieve_link_noise_margin_received_state( + status: FritzStatus, last_value: str +) -> float: + """Return download noise margin.""" + return status.noise_margin[1] # type: ignore[no-any-return] + + +def _retrieve_link_attenuation_sent_state( + status: FritzStatus, last_value: str +) -> float: + """Return upload line attenuation.""" + return status.attenuation[0] # type: ignore[no-any-return] + + +def _retrieve_link_attenuation_received_state( + status: FritzStatus, last_value: str +) -> float: + """Return download line attenuation.""" + return status.attenuation[1] # type: ignore[no-any-return] + + class SensorData(TypedDict, total=False): """Sensor data class.""" @@ -99,6 +138,7 @@ class SensorData(TypedDict, total=False): unit_of_measurement: str | None icon: str | None state_provider: Callable + connection_type: str | None SENSOR_DATA = { @@ -118,27 +158,27 @@ SENSOR_DATA = { state_provider=_retrieve_connection_uptime_state, ), "kb_s_sent": SensorData( - name="kB/s sent", + name="Upload Throughput", state_class=STATE_CLASS_MEASUREMENT, unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, icon="mdi:upload", state_provider=_retrieve_kb_s_sent_state, ), "kb_s_received": SensorData( - name="kB/s received", + name="Download Throughput", state_class=STATE_CLASS_MEASUREMENT, unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, icon="mdi:download", state_provider=_retrieve_kb_s_received_state, ), "max_kb_s_sent": SensorData( - name="Max kbit/s sent", + name="Max Connection Upload Throughput", unit_of_measurement=DATA_RATE_KILOBITS_PER_SECOND, icon="mdi:upload", state_provider=_retrieve_max_kb_s_sent_state, ), "max_kb_s_received": SensorData( - name="Max kbit/s received", + name="Max Connection Download Throughput", unit_of_measurement=DATA_RATE_KILOBITS_PER_SECOND, icon="mdi:download", state_provider=_retrieve_max_kb_s_received_state, @@ -159,6 +199,48 @@ SENSOR_DATA = { icon="mdi:download", state_provider=_retrieve_gb_received_state, ), + "link_kb_s_sent": SensorData( + name="Link Upload Throughput", + unit_of_measurement=DATA_RATE_KILOBITS_PER_SECOND, + icon="mdi:upload", + state_provider=_retrieve_link_kb_s_sent_state, + connection_type=DSL_CONNECTION, + ), + "link_kb_s_received": SensorData( + name="Link Download Throughput", + unit_of_measurement=DATA_RATE_KILOBITS_PER_SECOND, + icon="mdi:download", + state_provider=_retrieve_link_kb_s_received_state, + connection_type=DSL_CONNECTION, + ), + "link_noise_margin_sent": SensorData( + name="Link Upload Noise Margin", + unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + icon="mdi:upload", + state_provider=_retrieve_link_noise_margin_sent_state, + connection_type=DSL_CONNECTION, + ), + "link_noise_margin_received": SensorData( + name="Link Download Noise Margin", + unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + icon="mdi:download", + state_provider=_retrieve_link_noise_margin_received_state, + connection_type=DSL_CONNECTION, + ), + "link_attenuation_sent": SensorData( + name="Link Upload Power Attenuation", + unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + icon="mdi:upload", + state_provider=_retrieve_link_attenuation_sent_state, + connection_type=DSL_CONNECTION, + ), + "link_attenuation_received": SensorData( + name="Link Download Power Attenuation", + unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + icon="mdi:download", + state_provider=_retrieve_link_attenuation_received_state, + connection_type=DSL_CONNECTION, + ), } @@ -177,7 +259,16 @@ async def async_setup_entry( return entities = [] - for sensor_type in SENSOR_DATA: + dslinterface = await hass.async_add_executor_job( + fritzbox_tools.connection.call_action, + "WANDSLInterfaceConfig:1", + "GetInfo", + ) + dsl: bool = dslinterface["NewEnable"] + + for sensor_type, sensor_data in SENSOR_DATA.items(): + if not dsl and sensor_data.get("connection_type") == DSL_CONNECTION: + continue entities.append(FritzBoxSensor(fritzbox_tools, entry.title, sensor_type)) if entities: From a4fd718e41e53a3ad8be7eba4b98e6330eb579c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 8 Aug 2021 11:29:32 +0200 Subject: [PATCH 043/355] Fix device registry lookup in uptimerobot (#54256) --- homeassistant/components/uptimerobot/__init__.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/uptimerobot/__init__.py b/homeassistant/components/uptimerobot/__init__.py index 07782fda533..4e6ff7908ee 100644 --- a/homeassistant/components/uptimerobot/__init__.py +++ b/homeassistant/components/uptimerobot/__init__.py @@ -7,7 +7,11 @@ 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.device_registry import DeviceRegistry, async_get_registry +from homeassistant.helpers.device_registry import ( + DeviceRegistry, + async_entries_for_config_entry, + async_get_registry, +) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import API_ATTR_OK, COORDINATOR_UPDATE_INTERVAL, DOMAIN, LOGGER, PLATFORMS @@ -80,9 +84,9 @@ class UptimeRobotDataUpdateCoordinator(DataUpdateCoordinator): current_monitors = { list(device.identifiers)[0][1] - for device in self._device_registry.devices.values() - if self._config_entry_id in device.config_entries - and list(device.identifiers)[0][0] == DOMAIN + for device in async_entries_for_config_entry( + self._device_registry, self._config_entry_id + ) } new_monitors = {str(monitor.id) for monitor in monitors} if stale_monitors := current_monitors - new_monitors: From 3da61b77a90d3ee9042d1ed240768cde9f3e1ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 8 Aug 2021 12:26:14 +0200 Subject: [PATCH 044/355] Remove monitor checks in Uptime Robot entities (#54259) --- .../components/uptimerobot/entity.py | 54 ++++++------------- 1 file changed, 16 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/uptimerobot/entity.py b/homeassistant/components/uptimerobot/entity.py index b265af77535..b9783c88b9c 100644 --- a/homeassistant/components/uptimerobot/entity.py +++ b/homeassistant/components/uptimerobot/entity.py @@ -4,7 +4,7 @@ from __future__ import annotations from pyuptimerobot import UptimeRobotMonitor from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -25,56 +25,34 @@ class UptimeRobotEntity(CoordinatorEntity): """Initialize Uptime Robot entities.""" super().__init__(coordinator) self.entity_description = description - self._target = target + self._attr_device_info = { + "identifiers": {(DOMAIN, str(self.monitor.id))}, + "name": "Uptime Robot", + "manufacturer": "Uptime Robot Team", + "entry_type": "service", + "model": self.monitor.type.name, + } self._attr_extra_state_attributes = { ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_TARGET: self._target, + ATTR_TARGET: target, } + self._attr_unique_id = str(self.monitor.id) @property - def unique_id(self) -> str | None: - """Return the unique_id of the entity.""" - return str(self.monitor.id) if self.monitor else None - - @property - def device_info(self) -> DeviceInfo: - """Return device information about this AdGuard Home instance.""" - if self.monitor: - return { - "identifiers": {(DOMAIN, str(self.monitor.id))}, - "name": "Uptime Robot", - "manufacturer": "Uptime Robot Team", - "entry_type": "service", - "model": self.monitor.type.name, - } - return {} - - @property - def monitors(self) -> list[UptimeRobotMonitor]: + def _monitors(self) -> list[UptimeRobotMonitor]: """Return all monitors.""" return self.coordinator.data or [] @property - def monitor(self) -> UptimeRobotMonitor | None: + def monitor(self) -> UptimeRobotMonitor: """Return the monitor for this entity.""" return next( - ( - monitor - for monitor in self.monitors - if str(monitor.id) == self.entity_description.key - ), - None, + monitor + for monitor in self._monitors + if str(monitor.id) == self.entity_description.key ) @property def monitor_available(self) -> bool: """Returtn if the monitor is available.""" - status: bool = self.monitor.status == 2 if self.monitor else False - return status - - @property - def available(self) -> bool: - """Returtn if entity is available.""" - if not self.coordinator.last_update_success: - return False - return self.monitor is not None + return bool(self.monitor.status == 2) From 18a0fcf9311948c082d66fe17eb52e872303e954 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 8 Aug 2021 15:02:37 +0200 Subject: [PATCH 045/355] Strict typing for Neato (#53633) * Strict typing * Rebase * Tweak import * Cleanup * Rebase + typing hub * Flake8 * Update homeassistant/components/neato/config_flow.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/neato/vacuum.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/neato/camera.py Co-authored-by: Martin Hjelmare * Address review comments * Black * Update homeassistant/components/neato/config_flow.py Co-authored-by: Martin Hjelmare * Specific dict definition * Annotations Co-authored-by: Martin Hjelmare --- .strict-typing | 1 + homeassistant/components/neato/api.py | 9 +- homeassistant/components/neato/camera.py | 59 +++++--- homeassistant/components/neato/config_flow.py | 15 +- homeassistant/components/neato/hub.py | 10 +- homeassistant/components/neato/sensor.py | 45 ++++-- homeassistant/components/neato/switch.py | 50 +++--- homeassistant/components/neato/vacuum.py | 142 +++++++++++------- mypy.ini | 14 +- script/hassfest/mypy_config.py | 1 - 10 files changed, 213 insertions(+), 133 deletions(-) diff --git a/.strict-typing b/.strict-typing index 915ac50d6a1..e8c4f83fa80 100644 --- a/.strict-typing +++ b/.strict-typing @@ -63,6 +63,7 @@ homeassistant.components.mailbox.* homeassistant.components.media_player.* homeassistant.components.mysensors.* homeassistant.components.nam.* +homeassistant.components.neato.* homeassistant.components.nest.* homeassistant.components.netatmo.* homeassistant.components.network.* diff --git a/homeassistant/components/neato/api.py b/homeassistant/components/neato/api.py index a22b1b48e74..cd26b009040 100644 --- a/homeassistant/components/neato/api.py +++ b/homeassistant/components/neato/api.py @@ -1,5 +1,8 @@ """API for Neato Botvac bound to Home Assistant OAuth.""" +from __future__ import annotations + from asyncio import run_coroutine_threadsafe +from typing import Any import pybotvac @@ -7,7 +10,7 @@ from homeassistant import config_entries, core from homeassistant.helpers import config_entry_oauth2_flow -class ConfigEntryAuth(pybotvac.OAuthSession): +class ConfigEntryAuth(pybotvac.OAuthSession): # type: ignore[misc] """Provide Neato Botvac authentication tied to an OAuth2 based config entry.""" def __init__( @@ -29,7 +32,7 @@ class ConfigEntryAuth(pybotvac.OAuthSession): self.session.async_ensure_token_valid(), self.hass.loop ).result() - return self.session.token["access_token"] + return self.session.token["access_token"] # type: ignore[no-any-return] class NeatoImplementation(config_entry_oauth2_flow.LocalOAuth2Implementation): @@ -39,7 +42,7 @@ class NeatoImplementation(config_entry_oauth2_flow.LocalOAuth2Implementation): """ @property - def extra_authorize_data(self) -> dict: + def extra_authorize_data(self) -> dict[str, Any]: """Extra data that needs to be appended to the authorize url.""" return {"client_secret": self.client_secret} diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index 9a2f47bcfa3..b6def2cfe38 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -1,10 +1,20 @@ """Support for loading picture from Neato.""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import Any from pybotvac.exceptions import NeatoRobotException +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 .const import ( NEATO_DOMAIN, @@ -20,11 +30,13 @@ SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES) ATTR_GENERATED_AT = "generated_at" -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 Neato camera with config entry.""" dev = [] - neato = hass.data.get(NEATO_LOGIN) - mapdata = hass.data.get(NEATO_MAP_DATA) + neato: NeatoHub = hass.data[NEATO_LOGIN] + mapdata: dict[str, Any] | None = hass.data.get(NEATO_MAP_DATA) for robot in hass.data[NEATO_ROBOTS]: if "maps" in robot.traits: dev.append(NeatoCleaningMap(neato, robot, mapdata)) @@ -39,7 +51,9 @@ async def async_setup_entry(hass, entry, async_add_entities): class NeatoCleaningMap(Camera): """Neato cleaning map for last clean.""" - def __init__(self, neato, robot, mapdata): + def __init__( + self, neato: NeatoHub, robot: Robot, mapdata: dict[str, Any] | None + ) -> None: """Initialize Neato cleaning map.""" super().__init__() self.robot = robot @@ -47,24 +61,18 @@ class NeatoCleaningMap(Camera): self._mapdata = mapdata self._available = neato is not None self._robot_name = f"{self.robot.name} Cleaning Map" - self._robot_serial = self.robot.serial - self._generated_at = None - self._image_url = None - self._image = None + self._robot_serial: str = self.robot.serial + self._generated_at: str | None = None + self._image_url: str | None = None + self._image: bytes | None = None - def camera_image(self): + def camera_image(self) -> bytes | None: """Return image response.""" self.update() return self._image - def update(self): + def update(self) -> None: """Check the contents of the map list.""" - if self.neato is None: - _LOGGER.error("Error while updating '%s'", self.entity_id) - self._image = None - self._image_url = None - self._available = False - return _LOGGER.debug("Running camera update for '%s'", self.entity_id) try: @@ -80,7 +88,8 @@ class NeatoCleaningMap(Camera): return image_url = None - map_data = self._mapdata[self._robot_serial]["maps"][0] + if self._mapdata: + map_data: dict[str, Any] = self._mapdata[self._robot_serial]["maps"][0] image_url = map_data["url"] if image_url == self._image_url: _LOGGER.debug( @@ -89,7 +98,7 @@ class NeatoCleaningMap(Camera): return try: - image = self.neato.download_map(image_url) + image: HTTPResponse = self.neato.download_map(image_url) except NeatoRobotException as ex: if self._available: # Print only once when available _LOGGER.error( @@ -102,33 +111,33 @@ class NeatoCleaningMap(Camera): self._image = image.read() self._image_url = image_url - self._generated_at = map_data["generated_at"] + self._generated_at = map_data.get("generated_at") self._available = True @property - def name(self): + def name(self) -> str: """Return the name of this camera.""" return self._robot_name @property - def unique_id(self): + def unique_id(self) -> str: """Return unique ID.""" return self._robot_serial @property - def available(self): + def available(self) -> bool: """Return if the robot is available.""" return self._available @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device info for neato robot.""" return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}} @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the vacuum cleaner.""" - data = {} + data: dict[str, Any] = {} if self._generated_at is not None: data[ATTR_GENERATED_AT] = self._generated_at diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py index c4ca9e45a89..07aea0a7e9c 100644 --- a/homeassistant/components/neato/config_flow.py +++ b/homeassistant/components/neato/config_flow.py @@ -2,10 +2,13 @@ from __future__ import annotations import logging +from types import MappingProxyType +from typing import Any import voluptuous as vol from homeassistant.config_entries import SOURCE_REAUTH +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from .const import NEATO_DOMAIN @@ -23,7 +26,9 @@ class OAuth2FlowHandler( """Return logger.""" return logging.getLogger(__name__) - async def async_step_user(self, user_input: dict | None = None) -> dict: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Create an entry for the flow.""" current_entries = self._async_current_entries() if self.source != SOURCE_REAUTH and current_entries: @@ -32,11 +37,13 @@ class OAuth2FlowHandler( return await super().async_step_user(user_input=user_input) - async def async_step_reauth(self, data) -> dict: + async def async_step_reauth(self, data: MappingProxyType[str, Any]) -> FlowResult: """Perform reauth upon migration of old entries.""" return await self.async_step_reauth_confirm() - async def async_step_reauth_confirm(self, user_input: dict | None = None) -> dict: + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Confirm reauth upon migration of old entries.""" if user_input is None: return self.async_show_form( @@ -44,7 +51,7 @@ class OAuth2FlowHandler( ) return await self.async_step_user() - async def async_oauth_create_entry(self, data: dict) -> dict: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Create an entry for the flow. Update an entry if one already exist.""" current_entries = self._async_current_entries() if self.source == SOURCE_REAUTH and current_entries: diff --git a/homeassistant/components/neato/hub.py b/homeassistant/components/neato/hub.py index b394507f408..cb639de4acb 100644 --- a/homeassistant/components/neato/hub.py +++ b/homeassistant/components/neato/hub.py @@ -3,7 +3,9 @@ from datetime import timedelta import logging from pybotvac import Account +from urllib3.response import HTTPResponse +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.util import Throttle @@ -21,23 +23,23 @@ class NeatoHub: self.my_neato: Account = neato @Throttle(timedelta(minutes=1)) - def update_robots(self): + def update_robots(self) -> None: """Update the robot states.""" _LOGGER.debug("Running HUB.update_robots %s", self._hass.data.get(NEATO_ROBOTS)) self._hass.data[NEATO_ROBOTS] = self.my_neato.robots self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps - def download_map(self, url): + def download_map(self, url: str) -> HTTPResponse: """Download a new map image.""" map_image_data = self.my_neato.get_map_image(url) return map_image_data - async def async_update_entry_unique_id(self, entry) -> str: + async def async_update_entry_unique_id(self, entry: ConfigEntry) -> str: """Update entry for unique_id.""" await self._hass.async_add_executor_job(self.my_neato.refresh_userdata) - unique_id = self.my_neato.unique_id + unique_id: str = self.my_neato.unique_id if entry.unique_id == unique_id: return unique_id diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 98208698037..1cf10112b92 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -1,11 +1,20 @@ """Support for Neato sensors.""" +from __future__ import annotations + from datetime import timedelta import logging +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 PERCENTAGE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES @@ -16,10 +25,12 @@ SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES) BATTERY = "Battery" -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 the Neato sensor using config entry.""" dev = [] - neato = hass.data.get(NEATO_LOGIN) + neato: NeatoHub = hass.data[NEATO_LOGIN] for robot in hass.data[NEATO_ROBOTS]: dev.append(NeatoSensor(neato, robot)) @@ -33,15 +44,15 @@ async def async_setup_entry(hass, entry, async_add_entities): class NeatoSensor(SensorEntity): """Neato sensor.""" - def __init__(self, neato, robot): + def __init__(self, neato: NeatoHub, robot: Robot) -> None: """Initialize Neato sensor.""" self.robot = robot - self._available = False - self._robot_name = f"{self.robot.name} {BATTERY}" - self._robot_serial = self.robot.serial - self._state = None + self._available: bool = False + self._robot_name: str = f"{self.robot.name} {BATTERY}" + self._robot_serial: str = self.robot.serial + self._state: dict[str, Any] | None = None - def update(self): + def update(self) -> None: """Update Neato Sensor.""" try: self._state = self.robot.state @@ -58,36 +69,38 @@ class NeatoSensor(SensorEntity): _LOGGER.debug("self._state=%s", self._state) @property - def name(self): + def name(self) -> str: """Return the name of this sensor.""" return self._robot_name @property - def unique_id(self): + def unique_id(self) -> str: """Return unique ID.""" return self._robot_serial @property - def device_class(self): + def device_class(self) -> str: """Return the device class.""" return DEVICE_CLASS_BATTERY @property - def available(self): + def available(self) -> bool: """Return availability.""" return self._available @property - def state(self): + def state(self) -> str | None: """Return the state.""" - return self._state["details"]["charge"] if self._state else None + if self._state is not None: + return str(self._state["details"]["charge"]) + return None @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return unit of measurement.""" return PERCENTAGE @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device info for neato robot.""" return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}} diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index a3cc51b82c6..0e0d49f2b28 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -1,11 +1,19 @@ """Support for Neato Connected Vacuums switches.""" +from __future__ import annotations + from datetime import timedelta import logging +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 STATE_OFF, STATE_ON -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo, ToggleEntity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES @@ -18,10 +26,13 @@ SWITCH_TYPE_SCHEDULE = "schedule" SWITCH_TYPES = {SWITCH_TYPE_SCHEDULE: ["Schedule"]} -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 Neato switch with config entry.""" dev = [] - neato = hass.data.get(NEATO_LOGIN) + neato: NeatoHub = hass.data[NEATO_LOGIN] + for robot in hass.data[NEATO_ROBOTS]: for type_name in SWITCH_TYPES: dev.append(NeatoConnectedSwitch(neato, robot, type_name)) @@ -36,18 +47,18 @@ async def async_setup_entry(hass, entry, async_add_entities): class NeatoConnectedSwitch(ToggleEntity): """Neato Connected Switches.""" - def __init__(self, neato, robot, switch_type): + def __init__(self, neato: NeatoHub, robot: Robot, switch_type: str) -> None: """Initialize the Neato Connected switches.""" self.type = switch_type self.robot = robot self._available = False self._robot_name = f"{self.robot.name} {SWITCH_TYPES[self.type][0]}" - self._state = None - self._schedule_state = None + self._state: dict[str, Any] | None = None + self._schedule_state: str | None = None self._clean_state = None - self._robot_serial = self.robot.serial + self._robot_serial: str = self.robot.serial - def update(self): + def update(self) -> None: """Update the states of Neato switches.""" _LOGGER.debug("Running Neato switch update for '%s'", self.entity_id) try: @@ -65,7 +76,7 @@ class NeatoConnectedSwitch(ToggleEntity): _LOGGER.debug("self._state=%s", self._state) if self.type == SWITCH_TYPE_SCHEDULE: _LOGGER.debug("State: %s", self._state) - if self._state["details"]["isScheduleEnabled"]: + if self._state is not None and self._state["details"]["isScheduleEnabled"]: self._schedule_state = STATE_ON else: self._schedule_state = STATE_OFF @@ -74,34 +85,33 @@ class NeatoConnectedSwitch(ToggleEntity): ) @property - def name(self): + def name(self) -> str: """Return the name of the switch.""" return self._robot_name @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" return self._available @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique ID.""" return self._robot_serial @property - def is_on(self): + def is_on(self) -> bool: """Return true if switch is on.""" - if self.type == SWITCH_TYPE_SCHEDULE: - if self._schedule_state == STATE_ON: - return True - return False + return bool( + self.type == SWITCH_TYPE_SCHEDULE and self._schedule_state == STATE_ON + ) @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device info for neato robot.""" return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}} - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" if self.type == SWITCH_TYPE_SCHEDULE: try: @@ -111,7 +121,7 @@ class NeatoConnectedSwitch(ToggleEntity): "Neato switch connection error '%s': %s", self.entity_id, ex ) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" if self.type == SWITCH_TYPE_SCHEDULE: try: diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index b6cf43a6a3e..527cd4dce23 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -1,7 +1,11 @@ """Support for Neato Connected Vacuums.""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import Any +from pybotvac import Robot from pybotvac.exceptions import NeatoRobotException import voluptuous as vol @@ -24,9 +28,14 @@ from homeassistant.components.vacuum import ( SUPPORT_STOP, StateVacuumEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_MODE +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import NeatoHub from .const import ( ACTION, ALERTS, @@ -72,12 +81,14 @@ ATTR_CATEGORY = "category" ATTR_ZONE = "zone" -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 Neato vacuum with config entry.""" dev = [] - neato = hass.data.get(NEATO_LOGIN) - mapdata = hass.data.get(NEATO_MAP_DATA) - persistent_maps = hass.data.get(NEATO_PERSISTENT_MAPS) + neato: NeatoHub = hass.data[NEATO_LOGIN] + mapdata: dict[str, Any] | None = hass.data.get(NEATO_MAP_DATA) + persistent_maps: dict[str, Any] | None = hass.data.get(NEATO_PERSISTENT_MAPS) for robot in hass.data[NEATO_ROBOTS]: dev.append(NeatoConnectedVacuum(neato, robot, mapdata, persistent_maps)) @@ -105,33 +116,39 @@ async def async_setup_entry(hass, entry, async_add_entities): class NeatoConnectedVacuum(StateVacuumEntity): """Representation of a Neato Connected Vacuum.""" - def __init__(self, neato, robot, mapdata, persistent_maps): + def __init__( + self, + neato: NeatoHub, + robot: Robot, + mapdata: dict[str, Any] | None, + persistent_maps: dict[str, Any] | None, + ) -> None: """Initialize the Neato Connected Vacuum.""" self.robot = robot - self._available = neato is not None + self._available: bool = neato is not None self._mapdata = mapdata - self._name = f"{self.robot.name}" - self._robot_has_map = self.robot.has_persistent_maps + self._name: str = f"{self.robot.name}" + self._robot_has_map: bool = self.robot.has_persistent_maps self._robot_maps = persistent_maps - self._robot_serial = self.robot.serial - self._status_state = None - self._clean_state = None - self._state = None - self._clean_time_start = None - self._clean_time_stop = None - self._clean_area = None - self._clean_battery_start = None - self._clean_battery_end = None - self._clean_susp_charge_count = None - self._clean_susp_time = None - self._clean_pause_time = None - self._clean_error_time = None - self._launched_from = None - self._battery_level = None - self._robot_boundaries = [] - self._robot_stats = None + self._robot_serial: str = self.robot.serial + self._status_state: str | None = None + self._clean_state: str | None = None + self._state: dict[str, Any] | None = None + self._clean_time_start: str | None = None + self._clean_time_stop: str | None = None + self._clean_area: float | None = None + self._clean_battery_start: int | None = None + self._clean_battery_end: int | None = None + self._clean_susp_charge_count: int | None = None + self._clean_susp_time: int | None = None + self._clean_pause_time: int | None = None + self._clean_error_time: int | None = None + self._launched_from: str | None = None + self._battery_level: int | None = None + self._robot_boundaries: list = [] + self._robot_stats: dict[str, Any] | None = None - def update(self): + def update(self) -> None: """Update the states of Neato Vacuums.""" _LOGGER.debug("Running Neato Vacuums update for '%s'", self.entity_id) try: @@ -151,6 +168,8 @@ class NeatoConnectedVacuum(StateVacuumEntity): self._available = False return + if self._state is None: + return self._available = True _LOGGER.debug("self._state=%s", self._state) if "alert" in self._state: @@ -198,10 +217,12 @@ class NeatoConnectedVacuum(StateVacuumEntity): self._battery_level = self._state["details"]["charge"] - if not self._mapdata.get(self._robot_serial, {}).get("maps", []): + if self._mapdata is None or not self._mapdata.get(self._robot_serial, {}).get( + "maps", [] + ): return - mapdata = self._mapdata[self._robot_serial]["maps"][0] + mapdata: dict[str, Any] = self._mapdata[self._robot_serial]["maps"][0] self._clean_time_start = mapdata["start_at"] self._clean_time_stop = mapdata["end_at"] self._clean_area = mapdata["cleaned_area"] @@ -215,10 +236,11 @@ class NeatoConnectedVacuum(StateVacuumEntity): if ( self._robot_has_map + and self._state and self._state["availableServices"]["maps"] != "basic-1" - and self._robot_maps[self._robot_serial] + and self._robot_maps ): - allmaps = self._robot_maps[self._robot_serial] + allmaps: dict = self._robot_maps[self._robot_serial] _LOGGER.debug( "Found the following maps for '%s': %s", self.entity_id, allmaps ) @@ -249,44 +271,44 @@ class NeatoConnectedVacuum(StateVacuumEntity): ) @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return self._name @property - def supported_features(self): + def supported_features(self) -> int: """Flag vacuum cleaner robot features that are supported.""" return SUPPORT_NEATO @property - def battery_level(self): + def battery_level(self) -> int | None: """Return the battery level of the vacuum cleaner.""" return self._battery_level @property - def available(self): + def available(self) -> bool: """Return if the robot is available.""" return self._available @property - def icon(self): + def icon(self) -> str: """Return neato specific icon.""" return "mdi:robot-vacuum-variant" @property - def state(self): + def state(self) -> str | None: """Return the status of the vacuum cleaner.""" return self._clean_state @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique ID.""" return self._robot_serial @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the vacuum cleaner.""" - data = {} + data: dict[str, Any] = {} if self._status_state is not None: data[ATTR_STATUS] = self._status_state @@ -314,28 +336,32 @@ class NeatoConnectedVacuum(StateVacuumEntity): return data @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device info for neato robot.""" - info = {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}, "name": self._name} + info: DeviceInfo = { + "identifiers": {(NEATO_DOMAIN, self._robot_serial)}, + "name": self._name, + } if self._robot_stats: info["manufacturer"] = self._robot_stats["battery"]["vendor"] info["model"] = self._robot_stats["model"] info["sw_version"] = self._robot_stats["firmware"] return info - def start(self): + def start(self) -> None: """Start cleaning or resume cleaning.""" - try: - if self._state["state"] == 1: - self.robot.start_cleaning() - elif self._state["state"] == 3: - self.robot.resume_cleaning() - except NeatoRobotException as ex: - _LOGGER.error( - "Neato vacuum connection error for '%s': %s", self.entity_id, ex - ) + if self._state: + try: + if self._state["state"] == 1: + self.robot.start_cleaning() + elif self._state["state"] == 3: + self.robot.resume_cleaning() + except NeatoRobotException as ex: + _LOGGER.error( + "Neato vacuum connection error for '%s': %s", self.entity_id, ex + ) - def pause(self): + def pause(self) -> None: """Pause the vacuum.""" try: self.robot.pause_cleaning() @@ -344,7 +370,7 @@ class NeatoConnectedVacuum(StateVacuumEntity): "Neato vacuum connection error for '%s': %s", self.entity_id, ex ) - def return_to_base(self, **kwargs): + def return_to_base(self, **kwargs: Any) -> None: """Set the vacuum cleaner to return to the dock.""" try: if self._clean_state == STATE_CLEANING: @@ -356,7 +382,7 @@ class NeatoConnectedVacuum(StateVacuumEntity): "Neato vacuum connection error for '%s': %s", self.entity_id, ex ) - def stop(self, **kwargs): + def stop(self, **kwargs: Any) -> None: """Stop the vacuum cleaner.""" try: self.robot.stop_cleaning() @@ -365,7 +391,7 @@ class NeatoConnectedVacuum(StateVacuumEntity): "Neato vacuum connection error for '%s': %s", self.entity_id, ex ) - def locate(self, **kwargs): + def locate(self, **kwargs: Any) -> None: """Locate the robot by making it emit a sound.""" try: self.robot.locate() @@ -374,7 +400,7 @@ class NeatoConnectedVacuum(StateVacuumEntity): "Neato vacuum connection error for '%s': %s", self.entity_id, ex ) - def clean_spot(self, **kwargs): + def clean_spot(self, **kwargs: Any) -> None: """Run a spot cleaning starting from the base.""" try: self.robot.start_spot_cleaning() @@ -383,7 +409,9 @@ class NeatoConnectedVacuum(StateVacuumEntity): "Neato vacuum connection error for '%s': %s", self.entity_id, ex ) - def neato_custom_cleaning(self, mode, navigation, category, zone=None): + def neato_custom_cleaning( + self, mode: str, navigation: str, category: str, zone: str | None = None + ) -> None: """Zone cleaning service call.""" boundary_id = None if zone is not None: diff --git a/mypy.ini b/mypy.ini index 7f0a932f0af..9c54f7a043f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -704,6 +704,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.neato.*] +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.nest.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1515,9 +1526,6 @@ ignore_errors = true [mypy-homeassistant.components.mullvad.*] ignore_errors = true -[mypy-homeassistant.components.neato.*] -ignore_errors = true - [mypy-homeassistant.components.ness_alarm.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 23967721053..e3b76747be2 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -101,7 +101,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.mobile_app.*", "homeassistant.components.motion_blinds.*", "homeassistant.components.mullvad.*", - "homeassistant.components.neato.*", "homeassistant.components.ness_alarm.*", "homeassistant.components.nest.legacy.*", "homeassistant.components.netio.*", From aaddeb0bcdefa735a6b5c4be3f3f905537467518 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 8 Aug 2021 15:21:55 +0200 Subject: [PATCH 046/355] Add missing `motor_speed` sensor for Xiaomi Miio humidifier CA1 and CB1 (#54264) * Add motor_speed sensor for CA1 and CB1 * Remove value limits --- .../components/xiaomi_miio/sensor.py | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 852adfcc071..c180bb75a77 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -49,6 +49,8 @@ from .const import ( DOMAIN, KEY_COORDINATOR, KEY_DEVICE, + MODEL_AIRHUMIDIFIER_CA1, + MODEL_AIRHUMIDIFIER_CB1, MODELS_HUMIDIFIER_MIIO, MODELS_HUMIDIFIER_MIOT, MODELS_HUMIDIFIER_MJJSQ, @@ -69,13 +71,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -ATTR_ACTUAL_MOTOR_SPEED = "actual_speed" +ATTR_ACTUAL_SPEED = "actual_speed" ATTR_AIR_QUALITY = "air_quality" ATTR_CHARGING = "charging" ATTR_DISPLAY_CLOCK = "display_clock" ATTR_HUMIDITY = "humidity" ATTR_ILLUMINANCE = "illuminance" ATTR_LOAD_POWER = "load_power" +ATTR_MOTOR_SPEED = "motor_speed" ATTR_NIGHT_MODE = "night_mode" ATTR_NIGHT_TIME_BEGIN = "night_time_begin" ATTR_NIGHT_TIME_END = "night_time_end" @@ -130,14 +133,19 @@ SENSOR_TYPES = { valid_min_value=0.0, valid_max_value=100.0, ), - ATTR_ACTUAL_MOTOR_SPEED: XiaomiMiioSensorDescription( - key=ATTR_ACTUAL_MOTOR_SPEED, + ATTR_ACTUAL_SPEED: XiaomiMiioSensorDescription( + key=ATTR_ACTUAL_SPEED, name="Actual Speed", unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=STATE_CLASS_MEASUREMENT, - valid_min_value=200.0, - valid_max_value=2000.0, + ), + ATTR_MOTOR_SPEED: XiaomiMiioSensorDescription( + key=ATTR_MOTOR_SPEED, + name="Motor Speed", + unit_of_measurement="rpm", + icon="mdi:fast-forward", + state_class=STATE_CLASS_MEASUREMENT, ), ATTR_ILLUMINANCE: XiaomiMiioSensorDescription( key=ATTR_ILLUMINANCE, @@ -154,22 +162,15 @@ SENSOR_TYPES = { ), } -HUMIDIFIER_MIIO_SENSORS = { - ATTR_HUMIDITY: "humidity", - ATTR_TEMPERATURE: "temperature", -} - -HUMIDIFIER_MIOT_SENSORS = { - ATTR_HUMIDITY: "humidity", - ATTR_TEMPERATURE: "temperature", - ATTR_WATER_LEVEL: "water_level", - ATTR_ACTUAL_MOTOR_SPEED: "actual_speed", -} - -HUMIDIFIER_MJJSQ_SENSORS = { - ATTR_HUMIDITY: "humidity", - ATTR_TEMPERATURE: "temperature", -} +HUMIDIFIER_MIIO_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE) +HUMIDIFIER_CA1_CB1_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE, ATTR_MOTOR_SPEED) +HUMIDIFIER_MIOT_SENSORS = ( + ATTR_HUMIDITY, + ATTR_TEMPERATURE, + ATTR_WATER_LEVEL, + ATTR_ACTUAL_SPEED, +) +HUMIDIFIER_MJJSQ_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -225,7 +226,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): model = config_entry.data[CONF_MODEL] device = None sensors = [] - if model in MODELS_HUMIDIFIER_MIOT: + if model in (MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1): + device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] + sensors = HUMIDIFIER_CA1_CB1_SENSORS + elif model in MODELS_HUMIDIFIER_MIOT: device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] sensors = HUMIDIFIER_MIOT_SENSORS elif model in MODELS_HUMIDIFIER_MJJSQ: From 89bb95b0bee72c275d03bcc08df3f5f863bd10ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 8 Aug 2021 15:41:05 +0200 Subject: [PATCH 047/355] Add re-authentication to Uptime Robot (#54226) * Add reauthentication to Uptime Robot * Fix en strings * format * Fix docstring * Remove unused patch * Handle no existing entry * Handle account mismatch during reauthentication * Add test to validate reauth is triggered properly * Test reauth after setup * Adjust tests * Add full context for reauth init --- .../components/uptimerobot/__init__.py | 10 +- .../components/uptimerobot/config_flow.py | 42 +++- .../components/uptimerobot/strings.json | 45 ++-- .../uptimerobot/translations/en.json | 11 +- .../uptimerobot/test_config_flow.py | 197 +++++++++++++++++- tests/components/uptimerobot/test_init.py | 107 ++++++++++ 6 files changed, 383 insertions(+), 29 deletions(-) create mode 100644 tests/components/uptimerobot/test_init.py diff --git a/homeassistant/components/uptimerobot/__init__.py b/homeassistant/components/uptimerobot/__init__.py index 4e6ff7908ee..4eaef45c4d2 100644 --- a/homeassistant/components/uptimerobot/__init__.py +++ b/homeassistant/components/uptimerobot/__init__.py @@ -1,11 +1,17 @@ """The Uptime Robot integration.""" from __future__ import annotations -from pyuptimerobot import UptimeRobot, UptimeRobotException, UptimeRobotMonitor +from pyuptimerobot import ( + UptimeRobot, + UptimeRobotAuthenticationException, + UptimeRobotException, + UptimeRobotMonitor, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import ( DeviceRegistry, @@ -74,6 +80,8 @@ class UptimeRobotDataUpdateCoordinator(DataUpdateCoordinator): """Update data.""" try: response = await self._api.async_get_monitors() + except UptimeRobotAuthenticationException as exception: + raise ConfigEntryAuthFailed(exception) from exception except UptimeRobotException as exception: raise UpdateFailed(exception) from exception else: diff --git a/homeassistant/components/uptimerobot/config_flow.py b/homeassistant/components/uptimerobot/config_flow.py index 7bab74fa03e..1e8bec992ad 100644 --- a/homeassistant/components/uptimerobot/config_flow.py +++ b/homeassistant/components/uptimerobot/config_flow.py @@ -58,15 +58,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if response and response.data and response.data.email else None ) - if account: - await self.async_set_unique_id(str(account.user_id)) - self._abort_if_unique_id_configured() return errors, account async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: """Handle the initial step.""" - errors: dict[str, str] = {} if user_input is None: return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA @@ -74,12 +70,48 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors, account = await self._validate_input(user_input) if account: + await self.async_set_unique_id(str(account.user_id)) + self._abort_if_unique_id_configured() return self.async_create_entry(title=account.email, data=user_input) return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) + async def async_step_reauth( + self, user_input: ConfigType | None = None + ) -> FlowResult: + """Return the reauth confirm step.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: ConfigType | None = None + ) -> FlowResult: + """Dialog that informs the user that reauth is required.""" + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", data_schema=STEP_USER_DATA_SCHEMA + ) + errors, account = await self._validate_input(user_input) + if account: + if self.context.get("unique_id") and self.context["unique_id"] != str( + account.user_id + ): + errors["base"] = "reauth_failed_matching_account" + else: + existing_entry = await self.async_set_unique_id(str(account.user_id)) + if existing_entry: + self.hass.config_entries.async_update_entry( + existing_entry, data=user_input + ) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + return self.async_abort(reason="reauth_failed_existing") + + return self.async_show_form( + step_id="reauth_confirm", data_schema=STEP_USER_DATA_SCHEMA, 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(): @@ -93,5 +125,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _, account = await self._validate_input(imported_config) if account: + await self.async_set_unique_id(str(account.user_id)) + self._abort_if_unique_id_configured() return self.async_create_entry(title=account.email, data=imported_config) return self.async_abort(reason="unknown") diff --git a/homeassistant/components/uptimerobot/strings.json b/homeassistant/components/uptimerobot/strings.json index f51061eec33..094130b470d 100644 --- a/homeassistant/components/uptimerobot/strings.json +++ b/homeassistant/components/uptimerobot/strings.json @@ -1,20 +1,31 @@ { - "config": { - "step": { - "user": { - "data": { - "api_key": "[%key:common::config_flow::data::api_key%]" + "config": { + "step": { + "user": { + "description": "You need to supply a read-only API key from Uptime Robot", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "You need to supply a new read-only API key from Uptime Robot", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "reauth_failed_matching_account": "The API key you provided does not match the account ID for existing configuration." + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reauth_failed_existing": "Could not update the config entry, please remove the integration and set it up again.", + "unknown": "[%key:common::config_flow::error::unknown%]" } - } - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "unknown": "[%key:common::config_flow::error::unknown%]" } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/en.json b/homeassistant/components/uptimerobot/translations/en.json index d23431fa888..8140c84897f 100644 --- a/homeassistant/components/uptimerobot/translations/en.json +++ b/homeassistant/components/uptimerobot/translations/en.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Account is already configured", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", @@ -11,6 +12,14 @@ }, "step": { "user": { + "description": "You need to supply a read-only API key from Uptime Robot", + "data": { + "api_key": "API Key" + } + }, + "reauth_confirm": { + "title": "Reauthenticate Integration", + "description": "You need to supply a read-only API key from Uptime Robot", "data": { "api_key": "API Key" } diff --git a/tests/components/uptimerobot/test_config_flow.py b/tests/components/uptimerobot/test_config_flow.py index 41f0b6b639e..967e1b499f5 100644 --- a/tests/components/uptimerobot/test_config_flow.py +++ b/tests/components/uptimerobot/test_config_flow.py @@ -70,7 +70,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ) assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "cannot_connect"} + assert result2["errors"]["base"] == "cannot_connect" async def test_form_unexpected_error(hass: HomeAssistant) -> None: @@ -88,7 +88,7 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None: {"api_key": "1234"}, ) - assert result2["errors"] == {"base": "unknown"} + assert result2["errors"]["base"] == "unknown" async def test_form_api_key_error(hass: HomeAssistant) -> None: @@ -106,7 +106,7 @@ async def test_form_api_key_error(hass: HomeAssistant) -> None: {"api_key": "1234"}, ) - assert result2["errors"] == {"base": "invalid_api_key"} + assert result2["errors"]["base"] == "invalid_api_key" async def test_form_api_error(hass: HomeAssistant, caplog: LogCaptureFixture) -> None: @@ -129,7 +129,7 @@ async def test_form_api_error(hass: HomeAssistant, caplog: LogCaptureFixture) -> {"api_key": "1234"}, ) - assert result2["errors"] == {"base": "unknown"} + assert result2["errors"]["base"] == "unknown" assert "test error from API." in caplog.text @@ -211,7 +211,7 @@ async def test_user_unique_id_already_exists(hass): 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 with patch( @@ -233,5 +233,190 @@ async def test_user_unique_id_already_exists(hass): await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 - assert result2["type"] == "abort" + assert result2["type"] == RESULT_TYPE_ABORT assert result2["reason"] == "already_configured" + + +async def test_reauthentication(hass): + """Test Uptime Robot reauthentication.""" + old_entry = MockConfigEntry( + domain=DOMAIN, + data={"platform": DOMAIN, "api_key": "1234"}, + unique_id="1234567890", + ) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "reauth_confirm" + + with patch( + "pyuptimerobot.UptimeRobot.async_get_account_details", + return_value=UptimeRobotApiResponse.from_dict( + { + "stat": "ok", + "account": {"email": "test@test.test", "user_id": 1234567890}, + } + ), + ), patch( + "homeassistant.components.uptimerobot.async_setup_entry", + return_value=True, + ): + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"api_key": "1234"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_reauthentication_failure(hass): + """Test Uptime Robot reauthentication failure.""" + old_entry = MockConfigEntry( + domain=DOMAIN, + data={"platform": DOMAIN, "api_key": "1234"}, + unique_id="1234567890", + ) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "reauth_confirm" + + with patch( + "pyuptimerobot.UptimeRobot.async_get_account_details", + return_value=UptimeRobotApiResponse.from_dict( + { + "stat": "fail", + "error": {"message": "test error from API."}, + } + ), + ), patch( + "homeassistant.components.uptimerobot.async_setup_entry", + return_value=True, + ): + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"api_key": "1234"}, + ) + await hass.async_block_till_done() + + assert result2["step_id"] == "reauth_confirm" + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"]["base"] == "unknown" + + +async def test_reauthentication_failure_no_existing_entry(hass): + """Test Uptime Robot reauthentication with no existing entry.""" + old_entry = MockConfigEntry( + domain=DOMAIN, + data={"platform": DOMAIN, "api_key": "1234"}, + ) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "reauth_confirm" + + with patch( + "pyuptimerobot.UptimeRobot.async_get_account_details", + return_value=UptimeRobotApiResponse.from_dict( + { + "stat": "ok", + "account": {"email": "test@test.test", "user_id": 1234567890}, + } + ), + ), patch( + "homeassistant.components.uptimerobot.async_setup_entry", + return_value=True, + ): + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"api_key": "1234"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_failed_existing" + + +async def test_reauthentication_failure_account_not_matching(hass): + """Test Uptime Robot reauthentication failure when using another account.""" + old_entry = MockConfigEntry( + domain=DOMAIN, + data={"platform": DOMAIN, "api_key": "1234"}, + unique_id="1234567890", + ) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "reauth_confirm" + + with patch( + "pyuptimerobot.UptimeRobot.async_get_account_details", + return_value=UptimeRobotApiResponse.from_dict( + { + "stat": "ok", + "account": {"email": "test@test.test", "user_id": 1234567891}, + } + ), + ), patch( + "homeassistant.components.uptimerobot.async_setup_entry", + return_value=True, + ): + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"api_key": "1234"}, + ) + await hass.async_block_till_done() + + assert result2["step_id"] == "reauth_confirm" + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"]["base"] == "reauth_failed_matching_account" diff --git a/tests/components/uptimerobot/test_init.py b/tests/components/uptimerobot/test_init.py new file mode 100644 index 00000000000..b4534af763a --- /dev/null +++ b/tests/components/uptimerobot/test_init.py @@ -0,0 +1,107 @@ +"""Test the Uptime Robot init.""" +import datetime +from unittest.mock import patch + +from pytest import LogCaptureFixture +from pyuptimerobot import UptimeRobotApiResponse +from pyuptimerobot.exceptions import UptimeRobotAuthenticationException + +from homeassistant import config_entries +from homeassistant.components.uptimerobot.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.util import dt + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_reauthentication_trigger_in_setup( + hass: HomeAssistant, caplog: LogCaptureFixture +): + """Test reauthentication trigger.""" + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + title="test@test.test", + data={"platform": DOMAIN, "api_key": "1234"}, + unique_id="1234567890", + source=config_entries.SOURCE_USER, + ) + mock_config_entry.add_to_hass(hass) + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + side_effect=UptimeRobotAuthenticationException, + ): + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + + assert mock_config_entry.state == config_entries.ConfigEntryState.SETUP_ERROR + assert mock_config_entry.reason == "could not authenticate" + + assert len(flows) == 1 + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + assert flow["context"]["source"] == config_entries.SOURCE_REAUTH + assert flow["context"]["entry_id"] == mock_config_entry.entry_id + + assert ( + "Config entry 'test@test.test' for uptimerobot integration could not authenticate" + in caplog.text + ) + + +async def test_reauthentication_trigger_after_setup( + hass: HomeAssistant, caplog: LogCaptureFixture +): + """Test reauthentication trigger.""" + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + title="test@test.test", + data={"platform": DOMAIN, "api_key": "1234"}, + unique_id="1234567890", + source=config_entries.SOURCE_USER, + ) + mock_config_entry.add_to_hass(hass) + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + return_value=UptimeRobotApiResponse.from_dict( + { + "stat": "ok", + "monitors": [ + {"id": 1234, "friendly_name": "Test monitor", "status": 2} + ], + } + ), + ): + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + binary_sensor = hass.states.get("binary_sensor.test_monitor") + assert mock_config_entry.state == config_entries.ConfigEntryState.LOADED + assert binary_sensor.state == "on" + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + side_effect=UptimeRobotAuthenticationException, + ): + + async_fire_time_changed(hass, dt.utcnow() + datetime.timedelta(seconds=10)) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + binary_sensor = hass.states.get("binary_sensor.test_monitor") + + assert binary_sensor.state == "unavailable" + assert "Authentication failed while fetching uptimerobot data" in caplog.text + + assert len(flows) == 1 + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + assert flow["context"]["source"] == config_entries.SOURCE_REAUTH + assert flow["context"]["entry_id"] == mock_config_entry.entry_id From 7590cb2861f55d72c5f7b459e78d4f9f01b7a50c Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 8 Aug 2021 09:43:08 -0400 Subject: [PATCH 048/355] Fix camera state and attributes for agent_dvr (#54049) * Fix camera state and attributes for agent_dvr * tweak * tweak --- homeassistant/components/agent_dvr/camera.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/agent_dvr/camera.py b/homeassistant/components/agent_dvr/camera.py index 30c27eb047a..8a29428a833 100644 --- a/homeassistant/components/agent_dvr/camera.py +++ b/homeassistant/components/agent_dvr/camera.py @@ -67,8 +67,6 @@ async def async_setup_entry( class AgentCamera(MjpegCamera): """Representation of an Agent Device Stream.""" - _attr_supported_features = SUPPORT_ON_OFF - def __init__(self, device): """Initialize as a subclass of MjpegCamera.""" device_info = { @@ -80,7 +78,6 @@ class AgentCamera(MjpegCamera): self._removed = False self._attr_name = f"{device.client.name} {device.name}" self._attr_unique_id = f"{device._client.unique}_{device.typeID}_{device.id}" - self._attr_should_poll = True super().__init__(device_info) self._attr_device_info = { "identifiers": {(AGENT_DOMAIN, self.unique_id)}, @@ -102,10 +99,10 @@ class AgentCamera(MjpegCamera): if self.device.client.is_available and not self._removed: _LOGGER.error("%s lost", self.name) self._removed = True - self._attr_available = self.device.client.is_available self._attr_icon = "mdi:camcorder-off" if self.is_on: self._attr_icon = "mdi:camcorder" + self._attr_available = self.device.client.is_available self._attr_extra_state_attributes = { ATTR_ATTRIBUTION: ATTRIBUTION, "editable": False, @@ -117,6 +114,11 @@ class AgentCamera(MjpegCamera): "alerts_enabled": self.device.alerts_active, } + @property + def should_poll(self) -> bool: + """Update the state periodically.""" + return True + @property def is_recording(self) -> bool: """Return whether the monitor is recording.""" @@ -137,6 +139,11 @@ class AgentCamera(MjpegCamera): """Return True if entity is connected.""" return self.device.connected + @property + def supported_features(self) -> int: + """Return supported features.""" + return SUPPORT_ON_OFF + @property def is_on(self) -> bool: """Return true if on.""" From e8aa280d7f427e8d2f3536210456213dd4a0466f Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 8 Aug 2021 22:48:33 +0200 Subject: [PATCH 049/355] Add modbus get_hub (#54277) * Add dict with hubs. * Update flexit to use get_hub. * Remove executor_task for close. --- homeassistant/components/flexit/climate.py | 4 +-- homeassistant/components/modbus/__init__.py | 8 +++++- .../components/modbus/binary_sensor.py | 4 +-- homeassistant/components/modbus/climate.py | 4 +-- homeassistant/components/modbus/cover.py | 4 +-- homeassistant/components/modbus/fan.py | 5 ++-- homeassistant/components/modbus/light.py | 4 +-- homeassistant/components/modbus/modbus.py | 25 +++++++++---------- homeassistant/components/modbus/sensor.py | 4 +-- homeassistant/components/modbus/switch.py | 4 +-- 10 files changed, 36 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index 5e7ac137982..ce3e5a68e1e 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -11,13 +11,13 @@ from homeassistant.components.climate.const import ( SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) +from homeassistant.components.modbus import get_hub from homeassistant.components.modbus.const import ( CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, CALL_TYPE_WRITE_REGISTER, CONF_HUB, DEFAULT_HUB, - MODBUS_DOMAIN, ) from homeassistant.components.modbus.modbus import ModbusHub from homeassistant.const import ( @@ -53,7 +53,7 @@ async def async_setup_platform( """Set up the Flexit Platform.""" modbus_slave = config.get(CONF_SLAVE) name = config.get(CONF_NAME) - hub = hass.data[MODBUS_DOMAIN][config.get(CONF_HUB)] + hub = get_hub(hass, config[CONF_HUB]) async_add_entities([Flexit(hub, modbus_slave, name)], True) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 43aa49e6da7..8e7d1e48e1a 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -42,6 +42,7 @@ from homeassistant.const import ( CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from .const import ( @@ -114,7 +115,7 @@ from .const import ( DEFAULT_TEMP_UNIT, MODBUS_DOMAIN as DOMAIN, ) -from .modbus import async_modbus_setup +from .modbus import ModbusHub, async_modbus_setup from .validators import number_validator, scan_interval_validator, struct_validator _LOGGER = logging.getLogger(__name__) @@ -357,6 +358,11 @@ SERVICE_WRITE_COIL_SCHEMA = vol.Schema( ) +def get_hub(hass: HomeAssistant, name: str) -> ModbusHub: + """Return modbus hub with name.""" + return hass.data[DOMAIN][name] + + async def async_setup(hass, config): """Set up Modbus component.""" return await async_modbus_setup( diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index ac635c76275..a54630379b8 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -9,8 +9,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import get_hub from .base_platform import BasePlatform -from .const import MODBUS_DOMAIN PARALLEL_UPDATES = 1 _LOGGER = logging.getLogger(__name__) @@ -29,7 +29,7 @@ async def async_setup_platform( return for entry in discovery_info[CONF_BINARY_SENSORS]: - hub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] + hub = get_hub(hass, discovery_info[CONF_NAME]) sensors.append(ModbusBinarySensor(hub, entry)) async_add_entities(sensors) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 16334d883a9..692d3c9a058 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -23,6 +23,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import get_hub from .base_platform import BaseStructPlatform from .const import ( ATTR_TEMPERATURE, @@ -39,7 +40,6 @@ from .const import ( DATA_TYPE_UINT16, DATA_TYPE_UINT32, DATA_TYPE_UINT64, - MODBUS_DOMAIN, ) from .modbus import ModbusHub @@ -59,7 +59,7 @@ async def async_setup_platform( entities = [] for entity in discovery_info[CONF_CLIMATES]: - hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] + hub: ModbusHub = get_hub(hass, discovery_info[CONF_NAME]) entities.append(ModbusThermostat(hub, entity)) async_add_entities(entities) diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index 98a352f218a..e55fe6d92eb 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -19,6 +19,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import get_hub from .base_platform import BasePlatform from .const import ( CALL_TYPE_COIL, @@ -30,7 +31,6 @@ from .const import ( CONF_STATE_OPENING, CONF_STATUS_REGISTER, CONF_STATUS_REGISTER_TYPE, - MODBUS_DOMAIN, ) from .modbus import ModbusHub @@ -50,7 +50,7 @@ async def async_setup_platform( covers = [] for cover in discovery_info[CONF_COVERS]: - hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] + hub: ModbusHub = get_hub(hass, discovery_info[CONF_NAME]) covers.append(ModbusCover(hub, cover)) async_add_entities(covers) diff --git a/homeassistant/components/modbus/fan.py b/homeassistant/components/modbus/fan.py index a4d4265846d..435d331bbc4 100644 --- a/homeassistant/components/modbus/fan.py +++ b/homeassistant/components/modbus/fan.py @@ -8,8 +8,9 @@ from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType +from . import get_hub from .base_platform import BaseSwitch -from .const import CONF_FANS, MODBUS_DOMAIN +from .const import CONF_FANS from .modbus import ModbusHub PARALLEL_UPDATES = 1 @@ -25,7 +26,7 @@ async def async_setup_platform( fans = [] for entry in discovery_info[CONF_FANS]: - hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] + hub: ModbusHub = get_hub(hass, discovery_info[CONF_NAME]) fans.append(ModbusFan(hub, entry)) async_add_entities(fans) diff --git a/homeassistant/components/modbus/light.py b/homeassistant/components/modbus/light.py index 3eae5ed3db3..dd9a8ad754d 100644 --- a/homeassistant/components/modbus/light.py +++ b/homeassistant/components/modbus/light.py @@ -8,8 +8,8 @@ from homeassistant.const import CONF_LIGHTS, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType +from . import get_hub from .base_platform import BaseSwitch -from .const import MODBUS_DOMAIN from .modbus import ModbusHub PARALLEL_UPDATES = 1 @@ -25,7 +25,7 @@ async def async_setup_platform( lights = [] for entry in discovery_info[CONF_LIGHTS]: - hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] + hub: ModbusHub = get_hub(hass, discovery_info[CONF_NAME]) lights.append(ModbusLight(hub, entry)) async_add_entities(lights) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index dad91f26a12..e2f1295220f 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -1,4 +1,6 @@ """Support for Modbus.""" +from __future__ import annotations + import asyncio from collections import namedtuple import logging @@ -57,6 +59,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) + ConfEntry = namedtuple("ConfEntry", "call_type attr func_name") RunEntry = namedtuple("RunEntry", "attr func") PYMODBUS_CALL = [ @@ -184,6 +187,8 @@ async def async_modbus_setup( class ModbusHub: """Thread safe wrapper class for pymodbus.""" + name: str + def __init__(self, hass, client_config): """Initialize the Modbus hub.""" @@ -193,7 +198,7 @@ class ModbusHub: self._in_error = False self._lock = asyncio.Lock() self.hass = hass - self._config_name = client_config[CONF_NAME] + self.name = client_config[CONF_NAME] self._config_type = client_config[CONF_TYPE] self._config_delay = client_config[CONF_DELAY] self._pb_call = {} @@ -262,7 +267,7 @@ class ModbusHub: """Try to connect, and retry if needed.""" async with self._lock: if not await self.hass.async_add_executor_job(self._pymodbus_connect): - err = f"{self._config_name} connect failed, retry in pymodbus" + err = f"{self.name} connect failed, retry in pymodbus" self._log_error(err, error_state=False) return @@ -278,8 +283,11 @@ class ModbusHub: self._async_cancel_listener = None self._config_delay = 0 - def _pymodbus_close(self): - """Close sync. pymodbus.""" + async def async_close(self): + """Disconnect client.""" + if self._async_cancel_listener: + self._async_cancel_listener() + self._async_cancel_listener = None if self._client: try: self._client.close() @@ -287,15 +295,6 @@ class ModbusHub: self._log_error(str(exception_error)) self._client = None - async def async_close(self): - """Disconnect client.""" - if self._async_cancel_listener: - self._async_cancel_listener() - self._async_cancel_listener = None - - async with self._lock: - return await self.hass.async_add_executor_job(self._pymodbus_close) - def _pymodbus_connect(self): """Connect client.""" try: diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index e969fa23a65..7bb7e1cd049 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -10,8 +10,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import get_hub from .base_platform import BaseStructPlatform -from .const import MODBUS_DOMAIN from .modbus import ModbusHub PARALLEL_UPDATES = 1 @@ -31,7 +31,7 @@ async def async_setup_platform( return for entry in discovery_info[CONF_SENSORS]: - hub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] + hub = get_hub(hass, discovery_info[CONF_NAME]) sensors.append(ModbusRegisterSensor(hub, entry)) async_add_entities(sensors) diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 820e43419a0..55dc014420f 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -8,8 +8,8 @@ from homeassistant.const import CONF_NAME, CONF_SWITCHES from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType +from . import get_hub from .base_platform import BaseSwitch -from .const import MODBUS_DOMAIN from .modbus import ModbusHub PARALLEL_UPDATES = 1 @@ -26,7 +26,7 @@ async def async_setup_platform( return for entry in discovery_info[CONF_SWITCHES]: - hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] + hub: ModbusHub = get_hub(hass, discovery_info[CONF_NAME]) switches.append(ModbusSwitch(hub, entry)) async_add_entities(switches) From 02459e68132b255b0b8a069e88ff022d1eae3ee2 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 8 Aug 2021 23:23:21 +0200 Subject: [PATCH 050/355] Convert last properties in modbus to _attr_variable (#53919) --- .../components/modbus/base_platform.py | 71 +++++++++---------- .../components/modbus/binary_sensor.py | 11 +-- homeassistant/components/modbus/climate.py | 3 +- homeassistant/components/modbus/cover.py | 25 +++---- homeassistant/components/modbus/fan.py | 8 +++ homeassistant/components/modbus/sensor.py | 9 +-- 6 files changed, 53 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index f767201496c..c580e6167ca 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -21,7 +21,7 @@ from homeassistant.const import ( CONF_STRUCTURE, STATE_ON, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, ToggleEntity from homeassistant.helpers.event import async_call_later, async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity @@ -124,48 +124,46 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): registers = self._swap_registers(registers) byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers]) if self._data_type == DATA_TYPE_STRING: - self._value = byte_string.decode() - else: - val = struct.unpack(self._structure, byte_string) + return byte_string.decode() - # Issue: https://github.com/home-assistant/core/issues/41944 - # If unpack() returns a tuple greater than 1, don't try to process the value. - # Instead, return the values of unpack(...) separated by commas. - if len(val) > 1: - # Apply scale and precision to floats and ints - v_result = [] - for entry in val: - v_temp = self._scale * entry + self._offset - - # We could convert int to float, and the code would still work; however - # we lose some precision, and unit tests will fail. Therefore, we do - # the conversion only when it's absolutely necessary. - if isinstance(v_temp, int) and self._precision == 0: - v_result.append(str(v_temp)) - else: - v_result.append(f"{float(v_temp):.{self._precision}f}") - self._value = ",".join(map(str, v_result)) - else: - # Apply scale and precision to floats and ints - val = self._scale * val[0] + self._offset + val = struct.unpack(self._structure, byte_string) + # Issue: https://github.com/home-assistant/core/issues/41944 + # If unpack() returns a tuple greater than 1, don't try to process the value. + # Instead, return the values of unpack(...) separated by commas. + if len(val) > 1: + # Apply scale and precision to floats and ints + v_result = [] + for entry in val: + v_temp = self._scale * entry + self._offset # We could convert int to float, and the code would still work; however # we lose some precision, and unit tests will fail. Therefore, we do # the conversion only when it's absolutely necessary. - if isinstance(val, int) and self._precision == 0: - self._value = str(val) + if isinstance(v_temp, int) and self._precision == 0: + v_result.append(str(v_temp)) else: - self._value = f"{float(val):.{self._precision}f}" + v_result.append(f"{float(v_temp):.{self._precision}f}") + return ",".join(map(str, v_result)) + + # Apply scale and precision to floats and ints + val = self._scale * val[0] + self._offset + + # We could convert int to float, and the code would still work; however + # we lose some precision, and unit tests will fail. Therefore, we do + # the conversion only when it's absolutely necessary. + if isinstance(val, int) and self._precision == 0: + return str(val) + return f"{float(val):.{self._precision}f}" -class BaseSwitch(BasePlatform, RestoreEntity): +class BaseSwitch(BasePlatform, ToggleEntity, RestoreEntity): """Base class representing a Modbus switch.""" def __init__(self, hub: ModbusHub, config: dict) -> None: """Initialize the switch.""" config[CONF_INPUT_TYPE] = "" super().__init__(hub, config) - self._is_on = None + self._attr_is_on = False convert = { CALL_TYPE_REGISTER_HOLDING: ( CALL_TYPE_REGISTER_HOLDING, @@ -202,12 +200,7 @@ class BaseSwitch(BasePlatform, RestoreEntity): await self.async_base_added_to_hass() state = await self.async_get_last_state() if state: - self._is_on = state.state == STATE_ON - - @property - def is_on(self): - """Return true if switch is on.""" - return self._is_on + self._attr_is_on = state.state == STATE_ON async def async_turn(self, command): """Evaluate switch result.""" @@ -221,7 +214,7 @@ class BaseSwitch(BasePlatform, RestoreEntity): self._attr_available = True if not self._verify_active: - self._is_on = command == self.command_on + self._attr_is_on = command == self.command_on self.async_write_ha_state() return @@ -258,13 +251,13 @@ class BaseSwitch(BasePlatform, RestoreEntity): self._attr_available = True if self._verify_type == CALL_TYPE_COIL: - self._is_on = bool(result.bits[0] & 1) + self._attr_is_on = bool(result.bits[0] & 1) else: value = int(result.registers[0]) if value == self._state_on: - self._is_on = True + self._attr_is_on = True elif value == self._state_off: - self._is_on = False + self._attr_is_on = False elif value is not None: _LOGGER.error( "Unexpected response from modbus device slave %s register %s, got 0x%2x", diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index a54630379b8..08ebfc72880 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -43,14 +43,7 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity): await self.async_base_added_to_hass() state = await self.async_get_last_state() if state: - self._value = state.state == STATE_ON - else: - self._value = None - - @property - def is_on(self): - """Return the state of the sensor.""" - return self._value + self._attr_is_on = state.state == STATE_ON async def async_update(self, now=None): """Update the state of the sensor.""" @@ -68,6 +61,6 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity): self.async_write_ha_state() return - self._value = result.bits[0] & 1 + self._attr_is_on = result.bits[0] & 1 self._attr_available = True self.async_write_ha_state() diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 692d3c9a058..831f3c979cc 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -165,8 +165,7 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): self._attr_available = False return -1 - self.unpack_structure_result(result.registers) - + self._value = self.unpack_structure_result(result.registers) self._attr_available = True if self._value is None: diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index e55fe6d92eb..64165412d27 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -109,22 +109,13 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): STATE_UNAVAILABLE: None, STATE_UNKNOWN: None, } - self._value = convert[state.state] + self._set_attr_state(convert[state.state]) - @property - def is_opening(self): - """Return if the cover is opening or not.""" - return self._value == self._state_opening - - @property - def is_closing(self): - """Return if the cover is closing or not.""" - return self._value == self._state_closing - - @property - def is_closed(self): - """Return if the cover is closed or not.""" - return self._value == self._state_closed + def _set_attr_state(self, value): + """Convert received value to HA state.""" + self._attr_is_opening = value == self._state_opening + self._attr_is_closing = value == self._state_closing + self._attr_is_closed = value == self._state_closed async def async_open_cover(self, **kwargs: Any) -> None: """Open cover.""" @@ -160,7 +151,7 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): return None self._attr_available = True if self._input_type == CALL_TYPE_COIL: - self._value = bool(result.bits[0] & 1) + self._set_attr_state(bool(result.bits[0] & 1)) else: - self._value = int(result.registers[0]) + self._set_attr_state(int(result.registers[0])) self.async_write_ha_state() diff --git a/homeassistant/components/modbus/fan.py b/homeassistant/components/modbus/fan.py index 435d331bbc4..cf5c9762db8 100644 --- a/homeassistant/components/modbus/fan.py +++ b/homeassistant/components/modbus/fan.py @@ -43,3 +43,11 @@ class ModbusFan(BaseSwitch, FanEntity): ) -> None: """Set fan on.""" await self.async_turn(self.command_on) + + @property + def is_on(self): + """Return true if fan is on. + + This is needed due to the ongoing conversion of fan. + """ + return self._attr_is_on diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 7bb7e1cd049..fee3f53667d 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -54,12 +54,7 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): await self.async_base_added_to_hass() state = await self.async_get_last_state() if state: - self._value = state.state - - @property - def state(self): - """Return the state of the sensor.""" - return self._value + self._attr_state = state.state async def async_update(self, now=None): """Update the state of the sensor.""" @@ -73,6 +68,6 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): self.async_write_ha_state() return - self.unpack_structure_result(result.registers) + self._attr_state = self.unpack_structure_result(result.registers) self._attr_available = True self.async_write_ha_state() From 50068d2352ae745542e8bc1d25f1c2444df74bfc Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 8 Aug 2021 14:47:50 -0700 Subject: [PATCH 051/355] Bump google-nest-sdm to 0.3.6 (#54287) Add google-nest-sdm to 0.3.6 to include static typing fixes. --- 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 6c9462e43db..5b078393d1e 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.3.5"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.3.6"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 7a4f1153d70..a6695806dee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -708,7 +708,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.3.5 +google-nest-sdm==0.3.6 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bbd9cbbc7ff..8e490b65514 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -404,7 +404,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.3.5 +google-nest-sdm==0.3.6 # homeassistant.components.google_travel_time googlemaps==2.5.1 From 13737554446bd94a52f505fca4546cca1f98218c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 9 Aug 2021 00:11:34 +0000 Subject: [PATCH 052/355] [ci skip] Translation update --- .../uptimerobot/translations/ca.json | 13 +++++++++- .../uptimerobot/translations/en.json | 24 ++++++++++--------- .../uptimerobot/translations/et.json | 13 +++++++++- .../uptimerobot/translations/ru.json | 13 +++++++++- .../uptimerobot/translations/zh-Hant.json | 13 +++++++++- .../components/weather/translations/ru.json | 4 ++-- 6 files changed, 63 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/uptimerobot/translations/ca.json b/homeassistant/components/uptimerobot/translations/ca.json index ee0d2416cc6..a3bccb98295 100644 --- a/homeassistant/components/uptimerobot/translations/ca.json +++ b/homeassistant/components/uptimerobot/translations/ca.json @@ -2,18 +2,29 @@ "config": { "abort": { "already_configured": "El compte ja ha estat configurat", + "reauth_failed_existing": "No s'ha pogut actualitzar l'entrada de configuraci\u00f3, elimina la integraci\u00f3 i torna-la a instal\u00b7lar.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "unknown": "Error inesperat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_api_key": "Clau API inv\u00e0lida", + "reauth_failed_matching_account": "La clau API proporcionada no correspon amb l'identificador del compte de la configuraci\u00f3 actual.", "unknown": "Error inesperat" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Clau API" + }, + "description": "Has de proporcionar una nova clau API de nom\u00e9s lectura d'Uptime Robot", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "api_key": "Clau API" - } + }, + "description": "Has de proporcionar una clau API de nom\u00e9s lectura d'Uptime Robot" } } } diff --git a/homeassistant/components/uptimerobot/translations/en.json b/homeassistant/components/uptimerobot/translations/en.json index 8140c84897f..ae1a8cf5e45 100644 --- a/homeassistant/components/uptimerobot/translations/en.json +++ b/homeassistant/components/uptimerobot/translations/en.json @@ -2,27 +2,29 @@ "config": { "abort": { "already_configured": "Account is already configured", - "unknown": "Unexpected error", - "reauth_successful": "Re-authentication was successful" + "reauth_failed_existing": "Could not update the config entry, please remove the integration and set it up again.", + "reauth_successful": "Re-authentication was successful", + "unknown": "Unexpected error" }, "error": { "cannot_connect": "Failed to connect", "invalid_api_key": "Invalid API key", + "reauth_failed_matching_account": "The API key you provided does not match the account ID for existing configuration.", "unknown": "Unexpected error" }, "step": { - "user": { - "description": "You need to supply a read-only API key from Uptime Robot", - "data": { - "api_key": "API Key" - } - }, "reauth_confirm": { - "title": "Reauthenticate Integration", - "description": "You need to supply a read-only API key from Uptime Robot", "data": { "api_key": "API Key" - } + }, + "description": "You need to supply a new read-only API key from Uptime Robot", + "title": "Reauthenticate Integration" + }, + "user": { + "data": { + "api_key": "API Key" + }, + "description": "You need to supply a read-only API key from Uptime Robot" } } } diff --git a/homeassistant/components/uptimerobot/translations/et.json b/homeassistant/components/uptimerobot/translations/et.json index a0608c5fff6..c679ea3b19b 100644 --- a/homeassistant/components/uptimerobot/translations/et.json +++ b/homeassistant/components/uptimerobot/translations/et.json @@ -2,18 +2,29 @@ "config": { "abort": { "already_configured": "Konto on juba h\u00e4\u00e4lestatud", + "reauth_failed_existing": "Seadekirjet ei \u00f5nnestunud uuendada, eemalda sidumine ja seadista see uuesti.", + "reauth_successful": "Taastuvastamine \u00f5nnestus", "unknown": "Ootamatu t\u00f5rge" }, "error": { "cannot_connect": "\u00dchendamine nurjus", "invalid_api_key": "Vigane API v\u00f5ti", + "reauth_failed_matching_account": "Sisestatud API v\u00f5ti ei vasta olemasoleva konto ID s\u00e4tetele.", "unknown": "Ootamatu t\u00f5rge" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API v\u00f5ti" + }, + "description": "Pead sisestama uue Uptime Roboti kirjutuskaitstud API-v\u00f5tme", + "title": "Taastuvasta sidumine" + }, "user": { "data": { "api_key": "API v\u00f5ti" - } + }, + "description": "Pead sisestama Uptime Roboti kirjutuskaitstud API-v\u00f5tme" } } } diff --git a/homeassistant/components/uptimerobot/translations/ru.json b/homeassistant/components/uptimerobot/translations/ru.json index 60e7e8530d1..88da4b3b768 100644 --- a/homeassistant/components/uptimerobot/translations/ru.json +++ b/homeassistant/components/uptimerobot/translations/ru.json @@ -2,18 +2,29 @@ "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.", + "reauth_failed_existing": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438, \u0443\u0434\u0430\u043b\u0438\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0435\u0451 \u0441\u043d\u043e\u0432\u0430.", + "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.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "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_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "reauth_failed_matching_account": "\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0443 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0434\u043b\u044f \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\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": { + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043d\u043e\u0432\u044b\u0439 \u043a\u043b\u044e\u0447 API Uptime Robot \u0441 \u043f\u0440\u0430\u0432\u0430\u043c\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0447\u0442\u0435\u043d\u0438\u044f", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API" - } + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043b\u044e\u0447 API Uptime Robot \u0441 \u043f\u0440\u0430\u0432\u0430\u043c\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0447\u0442\u0435\u043d\u0438\u044f" } } } diff --git a/homeassistant/components/uptimerobot/translations/zh-Hant.json b/homeassistant/components/uptimerobot/translations/zh-Hant.json index c100c6868b9..73d27aac1db 100644 --- a/homeassistant/components/uptimerobot/translations/zh-Hant.json +++ b/homeassistant/components/uptimerobot/translations/zh-Hant.json @@ -2,18 +2,29 @@ "config": { "abort": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_failed_existing": "\u7121\u6cd5\u66f4\u65b0\u8a2d\u5b9a\u5be6\u9ad4\uff0c\u8acb\u79fb\u9664\u6574\u5408\u4e26\u91cd\u65b0\u8a2d\u5b9a\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "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", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u5bc6\u9470" + }, + "description": "\u9700\u8981\u63d0\u4f9b\u7531 Uptime Robot \u53d6\u5f97\u4e00\u7d44\u65b0\u7684\u552f\u8b80 API \u5bc6\u9470", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "api_key": "API \u5bc6\u9470" - } + }, + "description": "\u9700\u8981\u63d0\u4f9b\u7531 Uptime Robot \u53d6\u5f97\u552f\u8b80 API \u5bc6\u9470" } } } diff --git a/homeassistant/components/weather/translations/ru.json b/homeassistant/components/weather/translations/ru.json index d2d0a066874..1f0458b7653 100644 --- a/homeassistant/components/weather/translations/ru.json +++ b/homeassistant/components/weather/translations/ru.json @@ -6,8 +6,8 @@ "exceptional": "\u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435", "fog": "\u0422\u0443\u043c\u0430\u043d", "hail": "\u0413\u0440\u0430\u0434", - "lightning": "\u041c\u043e\u043b\u043d\u0438\u044f", - "lightning-rainy": "\u041c\u043e\u043b\u043d\u0438\u044f, \u0434\u043e\u0436\u0434\u044c", + "lightning": "\u0413\u0440\u043e\u0437\u0430", + "lightning-rainy": "\u0414\u043e\u0436\u0434\u044c \u0441 \u0433\u0440\u043e\u0437\u043e\u0439", "partlycloudy": "\u041f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0441\u0442\u044c", "pouring": "\u041b\u0438\u0432\u0435\u043d\u044c", "rainy": "\u0414\u043e\u0436\u0434\u044c", From 160bd74baefc4038fdad5f6eaeefbc67a42fd73e Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 8 Aug 2021 19:24:36 -0700 Subject: [PATCH 053/355] Update DeviceInfo static types (#54276) * Update nest static types from aditional PR feedback Update nest and device helper static types based on post-merge discussion in PR #53475 * Remove unused type: ignore in synology * Remove check for None device type Remove check for None device type in order to reduce untested code as this is a case not allowed by the nest python library. --- homeassistant/components/nest/device_info.py | 8 +++----- homeassistant/components/nest/legacy/__init__.py | 7 ++++--- homeassistant/components/synology_dsm/__init__.py | 14 +++++++------- homeassistant/helpers/entity.py | 10 +++++----- tests/components/nest/device_info_test.py | 4 ++-- tests/components/nest/sensor_sdm_test.py | 2 +- 6 files changed, 22 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/nest/device_info.py b/homeassistant/components/nest/device_info.py index 6278547f216..383c6d22258 100644 --- a/homeassistant/components/nest/device_info.py +++ b/homeassistant/components/nest/device_info.py @@ -40,7 +40,7 @@ class NestDeviceInfo: ) @property - def device_name(self) -> str: + def device_name(self) -> str | None: """Return the name of the physical device that includes the sensor.""" if InfoTrait.NAME in self._device.traits: trait: InfoTrait = self._device.traits[InfoTrait.NAME] @@ -56,11 +56,9 @@ class NestDeviceInfo: return self.device_model @property - def device_model(self) -> str: + def device_model(self) -> str | None: """Return device model information.""" # The API intentionally returns minimal information about specific # devices, instead relying on traits, but we can infer a generic model # name based on the type - if self._device.type in DEVICE_TYPE_MAP: - return DEVICE_TYPE_MAP[self._device.type] - return "Unknown" + return DEVICE_TYPE_MAP.get(self._device.type) diff --git a/homeassistant/components/nest/legacy/__init__.py b/homeassistant/components/nest/legacy/__init__.py index 04f7b1ac663..76ecf16b67b 100644 --- a/homeassistant/components/nest/legacy/__init__.py +++ b/homeassistant/components/nest/legacy/__init__.py @@ -9,6 +9,7 @@ from nest.nest import APIError, AuthorizationError import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, @@ -17,7 +18,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity @@ -96,7 +97,7 @@ def nest_update_event_broker(hass, nest): _LOGGER.debug("Stop listening for nest.update_event") -async def async_setup_legacy(hass, config) -> bool: +async def async_setup_legacy(hass: HomeAssistant, config: dict) -> bool: """Set up Nest components using the legacy nest API.""" if DOMAIN not in config: return True @@ -122,7 +123,7 @@ async def async_setup_legacy(hass, config) -> bool: return True -async def async_setup_legacy_entry(hass, entry) -> bool: +async def async_setup_legacy_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Nest from legacy config entry.""" nest = Nest(access_token=entry.data["tokens"]["access_token"]) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index a9ca7b4c48d..0bc88b683b7 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -686,10 +686,10 @@ class SynologyDSMDeviceEntity(SynologyDSMBaseEntity): """Initialize the Synology DSM disk or volume entity.""" super().__init__(api, entity_type, entity_info, coordinator) self._device_id = device_id - self._device_name = None - self._device_manufacturer = None - self._device_model = None - self._device_firmware = None + self._device_name: str | None = None + self._device_manufacturer: str | None = None + self._device_model: str | None = None + self._device_firmware: str | None = None self._device_type = None if "volume" in entity_type: @@ -730,8 +730,8 @@ class SynologyDSMDeviceEntity(SynologyDSMBaseEntity): (DOMAIN, f"{self._api.information.serial}_{self._device_id}") }, "name": f"Synology NAS ({self._device_name} - {self._device_type})", - "manufacturer": self._device_manufacturer, # type: ignore[typeddict-item] - "model": self._device_model, # type: ignore[typeddict-item] - "sw_version": self._device_firmware, # type: ignore[typeddict-item] + "manufacturer": self._device_manufacturer, + "model": self._device_model, + "sw_version": self._device_firmware, "via_device": (DOMAIN, self._api.information.serial), } diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 6383de15b4a..131460baa93 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -165,13 +165,13 @@ def get_unit_of_measurement(hass: HomeAssistant, entity_id: str) -> str | None: class DeviceInfo(TypedDict, total=False): """Entity device information for device registry.""" - name: str + name: str | None connections: set[tuple[str, str]] identifiers: set[tuple[str, str]] - manufacturer: str - model: str - suggested_area: str - sw_version: str + manufacturer: str | None + model: str | None + suggested_area: str | None + sw_version: str | None via_device: tuple[str, str] entry_type: str | None default_name: str diff --git a/tests/components/nest/device_info_test.py b/tests/components/nest/device_info_test.py index a0c6973c1d6..90b70f61d15 100644 --- a/tests/components/nest/device_info_test.py +++ b/tests/components/nest/device_info_test.py @@ -93,11 +93,11 @@ def test_device_invalid_type(): device_info = NestDeviceInfo(device) assert device_info.device_name == "My Doorbell" - assert device_info.device_model == "Unknown" + assert device_info.device_model is None assert device_info.device_brand == "Google Nest" assert device_info.device_info == { "identifiers": {("nest", "some-device-id")}, "name": "My Doorbell", "manufacturer": "Google Nest", - "model": "Unknown", + "model": None, } diff --git a/tests/components/nest/sensor_sdm_test.py b/tests/components/nest/sensor_sdm_test.py index cc18e8cd3ae..dfdfd58d546 100644 --- a/tests/components/nest/sensor_sdm_test.py +++ b/tests/components/nest/sensor_sdm_test.py @@ -208,5 +208,5 @@ async def test_device_with_unknown_type(hass): device_registry = dr.async_get(hass) device = device_registry.async_get(entry.device_id) assert device.name == "My Sensor" - assert device.model == "Unknown" + assert device.model is None assert device.identifiers == {("nest", "some-device-id")} From 5d56ce67f5a9b07c77bce0d1930072a6d4727863 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Aug 2021 22:33:40 -0500 Subject: [PATCH 054/355] Fix inconsistent supported_features return in demo lock (#54300) https://github.com/home-assistant/core/pull/51455#discussion_r684806197 --- homeassistant/components/demo/lock.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/demo/lock.py b/homeassistant/components/demo/lock.py index 7eabf9bea2d..af61c0f6111 100644 --- a/homeassistant/components/demo/lock.py +++ b/homeassistant/components/demo/lock.py @@ -97,3 +97,4 @@ class DemoLock(LockEntity): """Flag supported features.""" if self._openable: return SUPPORT_OPEN + return 0 From 557cc792e90ce5b759833fccd5b73d8e84a30b45 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 8 Aug 2021 20:33:47 -0700 Subject: [PATCH 055/355] Fix SQLAlchemy test warnings (#54116) --- homeassistant/components/history/__init__.py | 2 +- homeassistant/components/recorder/models.py | 3 +-- homeassistant/components/recorder/util.py | 4 +++- tests/components/recorder/test_util.py | 10 ++++++++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 3651dd8295f..a1e0fd45167 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -393,7 +393,7 @@ class Filters: if includes and not excludes: return or_(*includes) - if not excludes and includes: + if not includes and excludes: return not_(or_(*excludes)) return or_(*includes) & not_(or_(*excludes)) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 929115bdf25..ff64deb60cd 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -20,8 +20,7 @@ from sqlalchemy import ( distinct, ) from sqlalchemy.dialects import mysql -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relationship +from sqlalchemy.orm import declarative_base, relationship from sqlalchemy.orm.session import Session from homeassistant.const import ( diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 225eee6867f..e3af39b217a 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -10,6 +10,7 @@ import os import time from typing import TYPE_CHECKING, Callable +from sqlalchemy import text from sqlalchemy.exc import OperationalError, SQLAlchemyError from sqlalchemy.orm.session import Session @@ -332,4 +333,5 @@ def perodic_db_cleanups(instance: Recorder): if instance.engine.dialect.name == "sqlite": # Execute sqlite to create a wal checkpoint and free up disk space _LOGGER.debug("WAL checkpoint") - instance.engine.execute("PRAGMA wal_checkpoint(TRUNCATE);") + with instance.engine.connect() as connection: + connection.execute(text("PRAGMA wal_checkpoint(TRUNCATE);")) diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 5b4b234fbbb..cb54f0404b9 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock, patch import pytest from sqlalchemy import text +from sqlalchemy.sql.elements import TextClause from homeassistant.components.recorder import run_information_with_session, util from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX @@ -253,6 +254,11 @@ def test_end_incomplete_runs(hass_recorder, caplog): def test_perodic_db_cleanups(hass_recorder): """Test perodic db cleanups.""" hass = hass_recorder() - with patch.object(hass.data[DATA_INSTANCE].engine, "execute") as execute_mock: + with patch.object(hass.data[DATA_INSTANCE].engine, "connect") as connect_mock: util.perodic_db_cleanups(hass.data[DATA_INSTANCE]) - assert execute_mock.call_args[0][0] == "PRAGMA wal_checkpoint(TRUNCATE);" + + text_obj = connect_mock.return_value.__enter__.return_value.execute.mock_calls[0][ + 1 + ][0] + assert isinstance(text_obj, TextClause) + assert str(text_obj) == "PRAGMA wal_checkpoint(TRUNCATE);" From 2e903c92c475d217240311492e75077d28504972 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 9 Aug 2021 00:41:51 -0400 Subject: [PATCH 056/355] Add siren support for available tones provided as a dict (#54198) * Add siren support for available tones provided as a dict * remove paranthesis * hopefully make logic even easier to read * drop last parenthesis --- homeassistant/components/siren/__init__.py | 31 +++++++++++++++++----- tests/components/siren/test_init.py | 27 +++++++++++++++++-- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/siren/__init__.py b/homeassistant/components/siren/__init__.py index f301100fa6c..ed0e8b8645f 100644 --- a/homeassistant/components/siren/__init__.py +++ b/homeassistant/components/siren/__init__.py @@ -64,10 +64,29 @@ def process_turn_on_params( if not supported_features & SUPPORT_TONES: params.pop(ATTR_TONE, None) - elif (tone := params.get(ATTR_TONE)) is not None and ( - not siren.available_tones or tone not in siren.available_tones - ): - raise ValueError(f"Invalid tone received for entity {siren.entity_id}: {tone}") + elif (tone := params.get(ATTR_TONE)) is not None: + # Raise an exception if the specified tone isn't available + is_tone_dict_value = bool( + isinstance(siren.available_tones, dict) + and tone in siren.available_tones.values() + ) + if ( + not siren.available_tones + or tone not in siren.available_tones + and not is_tone_dict_value + ): + raise ValueError( + f"Invalid tone specified for entity {siren.entity_id}: {tone}, " + "check the available_tones attribute for valid tones to pass in" + ) + + # If available tones is a dict, and the tone provided is a dict value, we need + # to transform it to the corresponding dict key before returning + if is_tone_dict_value: + assert isinstance(siren.available_tones, dict) + params[ATTR_TONE] = next( + key for key, value in siren.available_tones.items() if value == tone + ) if not supported_features & SUPPORT_DURATION: params.pop(ATTR_DURATION, None) @@ -131,7 +150,7 @@ class SirenEntity(ToggleEntity): """Representation of a siren device.""" entity_description: SirenEntityDescription - _attr_available_tones: list[int | str] | None = None + _attr_available_tones: list[int | str] | dict[int, str] | None = None @final @property @@ -145,7 +164,7 @@ class SirenEntity(ToggleEntity): return None @property - def available_tones(self) -> list[int | str] | None: + def available_tones(self) -> list[int | str] | dict[int, str] | None: """ Return a list of available tones. diff --git a/tests/components/siren/test_init.py b/tests/components/siren/test_init.py index 729990ceaeb..e46fbbf8d5e 100644 --- a/tests/components/siren/test_init.py +++ b/tests/components/siren/test_init.py @@ -48,9 +48,32 @@ async def test_no_available_tones(hass): process_turn_on_params(siren, {"tone": "test"}) -async def test_missing_tones(hass): - """Test ValueError when setting a tone that is missing from available_tones.""" +async def test_available_tones_list(hass): + """Test that valid tones from tone list will get passed in.""" + siren = MockSirenEntity(SUPPORT_TONES, ["a", "b"]) + siren.hass = hass + assert process_turn_on_params(siren, {"tone": "a"}) == {"tone": "a"} + + +async def test_available_tones_dict(hass): + """Test that valid tones from available_tones dict will get passed in.""" + siren = MockSirenEntity(SUPPORT_TONES, {1: "a", 2: "b"}) + siren.hass = hass + assert process_turn_on_params(siren, {"tone": "a"}) == {"tone": 1} + assert process_turn_on_params(siren, {"tone": 1}) == {"tone": 1} + + +async def test_missing_tones_list(hass): + """Test ValueError when setting a tone that is missing from available_tones list.""" siren = MockSirenEntity(SUPPORT_TONES, ["a", "b"]) siren.hass = hass with pytest.raises(ValueError): process_turn_on_params(siren, {"tone": "test"}) + + +async def test_missing_tones_dict(hass): + """Test ValueError when setting a tone that is missing from available_tones dict.""" + siren = MockSirenEntity(SUPPORT_TONES, {1: "a", 2: "b"}) + siren.hass = hass + with pytest.raises(ValueError): + process_turn_on_params(siren, {"tone": 3}) From 0f68ebe92b4fc6372d8d380a55aafe4df5600285 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 9 Aug 2021 08:15:39 +0200 Subject: [PATCH 057/355] Add `unique_id` and `device_info` for SMS sensor (#54257) --- homeassistant/components/sms/sensor.py | 61 ++++++++++---------------- 1 file changed, 24 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/sms/sensor.py b/homeassistant/components/sms/sensor.py index fc2310426e3..eb6c6ab22e1 100644 --- a/homeassistant/components/sms/sensor.py +++ b/homeassistant/components/sms/sensor.py @@ -3,7 +3,7 @@ import logging import gammu # pylint: disable=import-error -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import DEVICE_CLASS_SIGNAL_STRENGTH, SIGNAL_STRENGTH_DECIBELS from .const import DOMAIN, SMS_GATEWAY @@ -14,48 +14,40 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the GSM Signal Sensor sensor.""" gateway = hass.data[DOMAIN][SMS_GATEWAY] - entities = [] imei = await gateway.get_imei_async() - name = f"gsm_signal_imei_{imei}" - entities.append( - GSMSignalSensor( - hass, - gateway, - name, - ) + async_add_entities( + [ + GSMSignalSensor( + hass, + gateway, + imei, + SensorEntityDescription( + key="signal", + name=f"gsm_signal_imei_{imei}", + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + entity_registry_enabled_default=False, + ), + ) + ], + True, ) - async_add_entities(entities, True) class GSMSignalSensor(SensorEntity): """Implementation of a GSM Signal sensor.""" - def __init__( - self, - hass, - gateway, - name, - ): + def __init__(self, hass, gateway, imei, description): """Initialize the GSM Signal sensor.""" + self._attr_device_info = { + "identifiers": {(DOMAIN, str(imei))}, + "name": "SMS Gateway", + } + self._attr_unique_id = str(imei) self._hass = hass self._gateway = gateway - self._name = name self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return SIGNAL_STRENGTH_DECIBELS - - @property - def device_class(self): - """Return the class of this sensor.""" - return DEVICE_CLASS_SIGNAL_STRENGTH + self.entity_description = description @property def available(self): @@ -78,8 +70,3 @@ class GSMSignalSensor(SensorEntity): def extra_state_attributes(self): """Return the sensor attributes.""" return self._state - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return False From a8354e729bf3c86115753fcd1e4e07109891c095 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 9 Aug 2021 02:21:07 -0500 Subject: [PATCH 058/355] Bump soco to 0.23.3 (#54288) --- 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 4ce5623ac38..d9c2a2cc6c9 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.23.2"], + "requirements": ["soco==0.23.3"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "zeroconf"], "zeroconf": ["_sonos._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index a6695806dee..0f2e0fdf1c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2152,7 +2152,7 @@ smhi-pkg==1.0.15 snapcast==2.1.3 # homeassistant.components.sonos -soco==0.23.2 +soco==0.23.3 # homeassistant.components.solaredge_local solaredge-local==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8e490b65514..5d224c4112d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1183,7 +1183,7 @@ smarthab==0.21 smhi-pkg==1.0.15 # homeassistant.components.sonos -soco==0.23.2 +soco==0.23.3 # homeassistant.components.solaredge solaredge==0.0.2 From 952d11cb0390852656206cd164db7099ea8e22ee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 9 Aug 2021 00:38:09 -0700 Subject: [PATCH 059/355] Ensure internal/external URL have no path (#54304) * Ensure internal/external URL have no path * Fix comment typo Co-authored-by: Martin Hjelmare --- homeassistant/components/config/core.py | 4 +- homeassistant/config.py | 125 ++++++++++++--------- homeassistant/core.py | 43 ++++--- homeassistant/helpers/config_validation.py | 10 ++ tests/helpers/test_config_validation.py | 19 ++++ tests/test_config.py | 13 +++ tests/test_core.py | 15 +++ 7 files changed, 161 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py index d9029dc497f..a6b39e556aa 100644 --- a/homeassistant/components/config/core.py +++ b/homeassistant/components/config/core.py @@ -44,8 +44,8 @@ class CheckConfigView(HomeAssistantView): vol.Optional("unit_system"): cv.unit_system, vol.Optional("location_name"): str, vol.Optional("time_zone"): cv.time_zone, - vol.Optional("external_url"): vol.Any(cv.url, None), - vol.Optional("internal_url"): vol.Any(cv.url, None), + vol.Optional("external_url"): vol.Any(cv.url_no_path, None), + vol.Optional("internal_url"): vol.Any(cv.url_no_path, None), vol.Optional("currency"): cv.currency, } ) diff --git a/homeassistant/config.py b/homeassistant/config.py index 12a39ab291b..cd159dfc8ce 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -10,6 +10,7 @@ import re import shutil from types import ModuleType from typing import Any, Callable +from urllib.parse import urlparse from awesomeversion import AwesomeVersion import voluptuous as vol @@ -161,6 +162,19 @@ def _no_duplicate_auth_mfa_module( return configs +def _filter_bad_internal_external_urls(conf: dict) -> dict: + """Filter internal/external URL with a path.""" + for key in CONF_INTERNAL_URL, CONF_EXTERNAL_URL: + if key in conf and urlparse(conf[key]).path not in ("", "/"): + # We warn but do not fix, because if this was incorrectly configured, + # adjusting this value might impact security. + _LOGGER.warning( + "Invalid %s set. It's not allowed to have a path (/bla)", key + ) + + return conf + + PACKAGES_CONFIG_SCHEMA = cv.schema_with_slug_keys( # Package names are slugs vol.Schema({cv.string: vol.Any(dict, list, None)}) # Component config ) @@ -188,59 +202,64 @@ CUSTOMIZE_CONFIG_SCHEMA = vol.Schema( } ) -CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend( - { - CONF_NAME: vol.Coerce(str), - CONF_LATITUDE: cv.latitude, - CONF_LONGITUDE: cv.longitude, - CONF_ELEVATION: vol.Coerce(int), - vol.Optional(CONF_TEMPERATURE_UNIT): cv.temperature_unit, - CONF_UNIT_SYSTEM: cv.unit_system, - CONF_TIME_ZONE: cv.time_zone, - vol.Optional(CONF_INTERNAL_URL): cv.url, - vol.Optional(CONF_EXTERNAL_URL): cv.url, - vol.Optional(CONF_ALLOWLIST_EXTERNAL_DIRS): vol.All( - cv.ensure_list, [vol.IsDir()] # pylint: disable=no-value-for-parameter - ), - vol.Optional(LEGACY_CONF_WHITELIST_EXTERNAL_DIRS): vol.All( - cv.ensure_list, [vol.IsDir()] # pylint: disable=no-value-for-parameter - ), - vol.Optional(CONF_ALLOWLIST_EXTERNAL_URLS): vol.All(cv.ensure_list, [cv.url]), - vol.Optional(CONF_PACKAGES, default={}): PACKAGES_CONFIG_SCHEMA, - vol.Optional(CONF_AUTH_PROVIDERS): vol.All( - cv.ensure_list, - [ - auth_providers.AUTH_PROVIDER_SCHEMA.extend( - { - CONF_TYPE: vol.NotIn( - ["insecure_example"], - "The insecure_example auth provider" - " is for testing only.", - ) - } - ) - ], - _no_duplicate_auth_provider, - ), - vol.Optional(CONF_AUTH_MFA_MODULES): vol.All( - cv.ensure_list, - [ - auth_mfa_modules.MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend( - { - CONF_TYPE: vol.NotIn( - ["insecure_example"], - "The insecure_example mfa module is for testing only.", - ) - } - ) - ], - _no_duplicate_auth_mfa_module, - ), - # pylint: disable=no-value-for-parameter - vol.Optional(CONF_MEDIA_DIRS): cv.schema_with_slug_keys(vol.IsDir()), - vol.Optional(CONF_LEGACY_TEMPLATES): cv.boolean, - vol.Optional(CONF_CURRENCY): cv.currency, - } +CORE_CONFIG_SCHEMA = vol.All( + CUSTOMIZE_CONFIG_SCHEMA.extend( + { + CONF_NAME: vol.Coerce(str), + CONF_LATITUDE: cv.latitude, + CONF_LONGITUDE: cv.longitude, + CONF_ELEVATION: vol.Coerce(int), + vol.Optional(CONF_TEMPERATURE_UNIT): cv.temperature_unit, + CONF_UNIT_SYSTEM: cv.unit_system, + CONF_TIME_ZONE: cv.time_zone, + vol.Optional(CONF_INTERNAL_URL): cv.url, + vol.Optional(CONF_EXTERNAL_URL): cv.url, + vol.Optional(CONF_ALLOWLIST_EXTERNAL_DIRS): vol.All( + cv.ensure_list, [vol.IsDir()] # pylint: disable=no-value-for-parameter + ), + vol.Optional(LEGACY_CONF_WHITELIST_EXTERNAL_DIRS): vol.All( + cv.ensure_list, [vol.IsDir()] # pylint: disable=no-value-for-parameter + ), + vol.Optional(CONF_ALLOWLIST_EXTERNAL_URLS): vol.All( + cv.ensure_list, [cv.url] + ), + vol.Optional(CONF_PACKAGES, default={}): PACKAGES_CONFIG_SCHEMA, + vol.Optional(CONF_AUTH_PROVIDERS): vol.All( + cv.ensure_list, + [ + auth_providers.AUTH_PROVIDER_SCHEMA.extend( + { + CONF_TYPE: vol.NotIn( + ["insecure_example"], + "The insecure_example auth provider" + " is for testing only.", + ) + } + ) + ], + _no_duplicate_auth_provider, + ), + vol.Optional(CONF_AUTH_MFA_MODULES): vol.All( + cv.ensure_list, + [ + auth_mfa_modules.MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend( + { + CONF_TYPE: vol.NotIn( + ["insecure_example"], + "The insecure_example mfa module is for testing only.", + ) + } + ) + ], + _no_duplicate_auth_mfa_module, + ), + # pylint: disable=no-value-for-parameter + vol.Optional(CONF_MEDIA_DIRS): cv.schema_with_slug_keys(vol.IsDir()), + vol.Optional(CONF_LEGACY_TEMPLATES): cv.boolean, + vol.Optional(CONF_CURRENCY): cv.currency, + } + ), + _filter_bad_internal_external_urls, ) diff --git a/homeassistant/core.py b/homeassistant/core.py index e2418321592..d95c25d93e2 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -19,6 +19,7 @@ import threading from time import monotonic from types import MappingProxyType from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, cast +from urllib.parse import urlparse import attr import voluptuous as vol @@ -1717,19 +1718,35 @@ class Config: ) data = await store.async_load() - if data: - self._update( - source=SOURCE_STORAGE, - latitude=data.get("latitude"), - longitude=data.get("longitude"), - elevation=data.get("elevation"), - unit_system=data.get("unit_system"), - location_name=data.get("location_name"), - time_zone=data.get("time_zone"), - external_url=data.get("external_url", _UNDEF), - internal_url=data.get("internal_url", _UNDEF), - currency=data.get("currency"), - ) + if not data: + return + + # In 2021.9 we fixed validation to disallow a path (because that's never correct) + # but this data still lives in storage, so we print a warning. + if "external_url" in data and urlparse(data["external_url"]).path not in ( + "", + "/", + ): + _LOGGER.warning("Invalid external_url set. It's not allowed to have a path") + + if "internal_url" in data and urlparse(data["internal_url"]).path not in ( + "", + "/", + ): + _LOGGER.warning("Invalid internal_url set. It's not allowed to have a path") + + self._update( + source=SOURCE_STORAGE, + latitude=data.get("latitude"), + longitude=data.get("longitude"), + elevation=data.get("elevation"), + unit_system=data.get("unit_system"), + location_name=data.get("location_name"), + time_zone=data.get("time_zone"), + external_url=data.get("external_url", _UNDEF), + internal_url=data.get("internal_url", _UNDEF), + currency=data.get("currency"), + ) async def async_store(self) -> None: """Store [homeassistant] core config.""" diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 66d1c01d6d3..f8d69a6e49a 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -649,6 +649,16 @@ def url(value: Any) -> str: raise vol.Invalid("invalid url") +def url_no_path(value: Any) -> str: + """Validate a url without a path.""" + url_in = url(value) + + if urlparse(url_in).path not in ("", "/"): + raise vol.Invalid("url it not allowed to have a path component") + + return url_in + + def x10_address(value: str) -> str: """Validate an x10 address.""" regex = re.compile(r"([A-Pa-p]{1})(?:[2-9]|1[0-6]?)$") diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index c5e9f5880c4..79b558e5083 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -120,6 +120,25 @@ def test_url(): assert schema(value) +def test_url_no_path(): + """Test URL.""" + schema = vol.Schema(cv.url_no_path) + + for value in ( + "https://localhost/test/index.html", + "http://home-assistant.io/test/", + ): + with pytest.raises(vol.MultipleInvalid): + schema(value) + + for value in ( + "http://localhost", + "http://home-assistant.io", + "https://community.home-assistant.io/", + ): + assert schema(value) + + def test_platform_config(): """Test platform config validation.""" options = ({}, {"hello": "world"}) diff --git a/tests/test_config.py b/tests/test_config.py index 96196c943aa..441029d27dc 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -215,6 +215,19 @@ def test_core_config_schema(): ) +def test_core_config_schema_internal_external_warning(caplog): + """Test that we warn for internal/external URL with path.""" + config_util.CORE_CONFIG_SCHEMA( + { + "external_url": "https://www.example.com/bla", + "internal_url": "http://example.local/yo", + } + ) + + assert "Invalid external_url set" in caplog.text + assert "Invalid internal_url set" in caplog.text + + def test_customize_dict_schema(): """Test basic customize config validation.""" values = ({ATTR_FRIENDLY_NAME: None}, {ATTR_ASSUMED_STATE: "2"}) diff --git a/tests/test_core.py b/tests/test_core.py index 77ec07e6a63..df66fedd025 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1374,6 +1374,21 @@ async def test_additional_data_in_core_config(hass, hass_storage): assert config.location_name == "Test Name" +async def test_incorrect_internal_external_url(hass, hass_storage, caplog): + """Test that we warn when detecting invalid internal/extenral url.""" + config = ha.Config(hass) + hass_storage[ha.CORE_STORAGE_KEY] = { + "version": 1, + "data": { + "internal_url": "https://community.home-assistant.io/profile", + "external_url": "https://www.home-assistant.io/blue", + }, + } + await config.async_load() + assert "Invalid external_url set" in caplog.text + assert "Invalid internal_url set" in caplog.text + + async def test_start_events(hass): """Test events fired when starting Home Assistant.""" hass.state = ha.CoreState.not_running From a1abd4f0d61b42191a71c0a44d7ce4064aff718a Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 9 Aug 2021 10:52:14 +0200 Subject: [PATCH 060/355] Fix external internal url core check (#54310) --- homeassistant/core.py | 4 ++-- tests/test_core.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index d95c25d93e2..1b1849ba548 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1723,13 +1723,13 @@ class Config: # In 2021.9 we fixed validation to disallow a path (because that's never correct) # but this data still lives in storage, so we print a warning. - if "external_url" in data and urlparse(data["external_url"]).path not in ( + if data.get("external_url") and urlparse(data["external_url"]).path not in ( "", "/", ): _LOGGER.warning("Invalid external_url set. It's not allowed to have a path") - if "internal_url" in data and urlparse(data["internal_url"]).path not in ( + if data.get("internal_url") and urlparse(data["internal_url"]).path not in ( "", "/", ): diff --git a/tests/test_core.py b/tests/test_core.py index df66fedd025..641a5e0dfda 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1377,6 +1377,18 @@ async def test_additional_data_in_core_config(hass, hass_storage): async def test_incorrect_internal_external_url(hass, hass_storage, caplog): """Test that we warn when detecting invalid internal/extenral url.""" config = ha.Config(hass) + + hass_storage[ha.CORE_STORAGE_KEY] = { + "version": 1, + "data": { + "internal_url": None, + "external_url": None, + }, + } + await config.async_load() + assert "Invalid external_url set" not in caplog.text + assert "Invalid internal_url set" not in caplog.text + hass_storage[ha.CORE_STORAGE_KEY] = { "version": 1, "data": { From d97f93933f28784f0a5e962f9784f42f6f4af568 Mon Sep 17 00:00:00 2001 From: ZeGuigui Date: Mon, 9 Aug 2021 11:38:16 +0200 Subject: [PATCH 061/355] Fix atom integration for long term statistics (#54285) * Fix atom integration for long term statistics * Remove commented code * Fix last_reset syntax * last_reset not an extra attribute * last_reset as utc * black formatting * isort fix --- homeassistant/components/atome/sensor.py | 40 +++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index bcb7b4f1ece..7295a9cee41 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -14,12 +14,13 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_USERNAME, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, POWER_WATT, ) import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle +from homeassistant.util import Throttle, dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -87,12 +88,16 @@ class AtomeData: self._is_connected = None self._day_usage = None self._day_price = None + self._day_last_reset = None self._week_usage = None self._week_price = None + self._week_last_reset = None self._month_usage = None self._month_price = None + self._month_last_reset = None self._year_usage = None self._year_price = None + self._year_last_reset = None @property def live_power(self): @@ -137,6 +142,11 @@ class AtomeData: """Return latest daily usage value.""" return self._day_price + @property + def day_last_reset(self): + """Return latest daily last reset.""" + return self._day_last_reset + @Throttle(DAILY_SCAN_INTERVAL) def update_day_usage(self): """Return current daily power usage.""" @@ -144,6 +154,7 @@ class AtomeData: values = self.atome_client.get_consumption(DAILY_TYPE) self._day_usage = values["total"] / 1000 self._day_price = values["price"] + self._day_last_reset = dt_util.parse_datetime(values["startPeriod"]) _LOGGER.debug("Updating Atome daily data. Got: %d", self._day_usage) except KeyError as error: @@ -159,6 +170,11 @@ class AtomeData: """Return latest weekly usage value.""" return self._week_price + @property + def week_last_reset(self): + """Return latest weekly last reset value.""" + return self._week_last_reset + @Throttle(WEEKLY_SCAN_INTERVAL) def update_week_usage(self): """Return current weekly power usage.""" @@ -166,6 +182,7 @@ class AtomeData: values = self.atome_client.get_consumption(WEEKLY_TYPE) self._week_usage = values["total"] / 1000 self._week_price = values["price"] + self._week_last_reset = dt_util.parse_datetime(values["startPeriod"]) _LOGGER.debug("Updating Atome weekly data. Got: %d", self._week_usage) except KeyError as error: @@ -181,6 +198,11 @@ class AtomeData: """Return latest monthly usage value.""" return self._month_price + @property + def month_last_reset(self): + """Return latest monthly last reset value.""" + return self._month_last_reset + @Throttle(MONTHLY_SCAN_INTERVAL) def update_month_usage(self): """Return current monthly power usage.""" @@ -188,6 +210,7 @@ class AtomeData: values = self.atome_client.get_consumption(MONTHLY_TYPE) self._month_usage = values["total"] / 1000 self._month_price = values["price"] + self._month_last_reset = dt_util.parse_datetime(values["startPeriod"]) _LOGGER.debug("Updating Atome monthly data. Got: %d", self._month_usage) except KeyError as error: @@ -203,6 +226,11 @@ class AtomeData: """Return latest yearly usage value.""" return self._year_price + @property + def year_last_reset(self): + """Return latest yearly last reset value.""" + return self._year_last_reset + @Throttle(YEARLY_SCAN_INTERVAL) def update_year_usage(self): """Return current yearly power usage.""" @@ -210,6 +238,7 @@ class AtomeData: values = self.atome_client.get_consumption(YEARLY_TYPE) self._year_usage = values["total"] / 1000 self._year_price = values["price"] + self._year_last_reset = dt_util.parse_datetime(values["startPeriod"]) _LOGGER.debug("Updating Atome yearly data. Got: %d", self._year_usage) except KeyError as error: @@ -219,19 +248,19 @@ class AtomeData: class AtomeSensor(SensorEntity): """Representation of a sensor entity for Atome.""" - _attr_device_class = DEVICE_CLASS_POWER - def __init__(self, data, name, sensor_type): """Initialize the sensor.""" self._attr_name = name self._data = data self._sensor_type = sensor_type + self._attr_state_class = STATE_CLASS_MEASUREMENT if sensor_type == LIVE_TYPE: + self._attr_device_class = DEVICE_CLASS_POWER self._attr_unit_of_measurement = POWER_WATT - self._attr_state_class = STATE_CLASS_MEASUREMENT else: + self._attr_device_class = DEVICE_CLASS_ENERGY self._attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR def update(self): @@ -247,6 +276,9 @@ class AtomeSensor(SensorEntity): } else: self._attr_state = getattr(self._data, f"{self._sensor_type}_usage") + self._attr_last_reset = dt_util.as_utc( + getattr(self._data, f"{self._sensor_type}_last_reset") + ) self._attr_extra_state_attributes = { "price": getattr(self._data, f"{self._sensor_type}_price") } From e7f0768ae67f71d5a8a2e5dad57434bb105d6f44 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 9 Aug 2021 12:11:54 +0200 Subject: [PATCH 062/355] Convert base_config_test in modbus to existing Pytest.fixture (#53836) * Convert base_config_test to pytest.fixture. --- tests/components/modbus/conftest.py | 49 +++------- tests/components/modbus/test_sensor.py | 121 +++++++++++++------------ 2 files changed, 77 insertions(+), 93 deletions(-) diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index db960f448ff..86eff5e44ad 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -39,9 +39,15 @@ def mock_pymodbus(): yield mock_pb -@pytest.fixture -async def mock_modbus(hass, do_config): +@pytest.fixture( + params=[ + {"testLoad": True}, + ], +) +async def mock_modbus(hass, caplog, request, do_config): """Load integration modbus using mocked pymodbus.""" + + caplog.set_level(logging.WARNING) config = { DOMAIN: [ { @@ -56,7 +62,10 @@ async def mock_modbus(hass, do_config): with mock.patch( "homeassistant.components.modbus.modbus.ModbusTcpClient", autospec=True ) as mock_pb: - assert await async_setup_component(hass, DOMAIN, config) is True + if request.param["testLoad"]: + assert await async_setup_component(hass, DOMAIN, config) is True + else: + await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() yield mock_pb @@ -88,7 +97,6 @@ async def base_test( register_words, expected, method_discovery=False, - check_config_only=False, config_modbus=None, scan_interval=None, expect_init_to_fail=False, @@ -173,8 +181,6 @@ async def base_test( assert device is None elif device is None: pytest.fail("CONFIG failed, see output") - if check_config_only: - return # Trigger update call with time_changed event now = now + timedelta(seconds=scan_interval + 60) @@ -187,37 +193,6 @@ async def base_test( return hass.states.get(entity_id).state -async def base_config_test( - hass, - config_device, - device_name, - entity_domain, - array_name_discovery, - array_name_old_config, - method_discovery=False, - config_modbus=None, - expect_init_to_fail=False, - expect_setup_to_fail=False, -): - """Check config of device for given config.""" - - await base_test( - hass, - config_device, - device_name, - entity_domain, - array_name_discovery, - array_name_old_config, - None, - None, - method_discovery=method_discovery, - check_config_only=True, - config_modbus=config_modbus, - expect_init_to_fail=expect_init_to_fail, - expect_setup_to_fail=expect_setup_to_fail, - ) - - async def prepare_service_update(hass, config): """Run test for service write_coil.""" diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index f01a3ef9da5..a5ec79d62e4 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -1,6 +1,4 @@ """The tests for the Modbus sensor component.""" -import logging - import pytest from homeassistant.components.modbus.const import ( @@ -37,7 +35,7 @@ from homeassistant.const import ( ) from homeassistant.core import State -from .conftest import ReadResult, base_config_test, base_test, prepare_service_update +from .conftest import ReadResult, base_test, prepare_service_update SENSOR_NAME = "test_sensor" ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" @@ -133,95 +131,106 @@ async def test_config_sensor(hass, mock_modbus): assert SENSOR_DOMAIN in hass.config.components +@pytest.mark.parametrize("mock_modbus", [{"testLoad": False}], indirect=True) @pytest.mark.parametrize( "do_config,error_message", [ ( { - CONF_ADDRESS: 1234, - CONF_COUNT: 8, - CONF_PRECISION: 2, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, - CONF_STRUCTURE: ">no struct", + CONF_SENSORS: [ + { + CONF_NAME: SENSOR_NAME, + CONF_ADDRESS: 1234, + CONF_COUNT: 8, + CONF_PRECISION: 2, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_STRUCTURE: ">no struct", + }, + ] }, "bad char in struct format", ), ( { - CONF_ADDRESS: 1234, - CONF_COUNT: 2, - CONF_PRECISION: 2, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, - CONF_STRUCTURE: ">4f", + CONF_SENSORS: [ + { + CONF_NAME: SENSOR_NAME, + CONF_ADDRESS: 1234, + CONF_COUNT: 2, + CONF_PRECISION: 2, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_STRUCTURE: ">4f", + }, + ] }, "Structure request 16 bytes, but 2 registers have a size of 4 bytes", ), ( { - CONF_ADDRESS: 1234, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, - CONF_COUNT: 4, - CONF_SWAP: CONF_SWAP_NONE, - CONF_STRUCTURE: "invalid", + CONF_SENSORS: [ + { + CONF_NAME: SENSOR_NAME, + CONF_ADDRESS: 1234, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_COUNT: 4, + CONF_SWAP: CONF_SWAP_NONE, + CONF_STRUCTURE: "invalid", + }, + ] }, "bad char in struct format", ), ( { - CONF_ADDRESS: 1234, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, - CONF_COUNT: 4, - CONF_SWAP: CONF_SWAP_NONE, - CONF_STRUCTURE: "", + CONF_SENSORS: [ + { + CONF_NAME: SENSOR_NAME, + CONF_ADDRESS: 1234, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_COUNT: 4, + CONF_SWAP: CONF_SWAP_NONE, + CONF_STRUCTURE: "", + }, + ] }, "Error in sensor test_sensor. The `structure` field can not be empty", ), ( { - CONF_ADDRESS: 1234, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, - CONF_COUNT: 4, - CONF_SWAP: CONF_SWAP_NONE, - CONF_STRUCTURE: "1s", + CONF_SENSORS: [ + { + CONF_NAME: SENSOR_NAME, + CONF_ADDRESS: 1234, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_COUNT: 4, + CONF_SWAP: CONF_SWAP_NONE, + CONF_STRUCTURE: "1s", + }, + ] }, "Structure request 1 bytes, but 4 registers have a size of 8 bytes", ), ( { - CONF_ADDRESS: 1234, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, - CONF_COUNT: 1, - CONF_STRUCTURE: "2s", - CONF_SWAP: CONF_SWAP_WORD, + CONF_SENSORS: [ + { + CONF_NAME: SENSOR_NAME, + CONF_ADDRESS: 1234, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_COUNT: 1, + CONF_STRUCTURE: "2s", + CONF_SWAP: CONF_SWAP_WORD, + }, + ] }, "Error in sensor test_sensor swap(word) not possible due to the registers count: 1, needed: 2", ), ], ) -async def test_config_wrong_struct_sensor( - hass, caplog, do_config, error_message, mock_pymodbus -): +async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, caplog): """Run test for sensor with wrong struct.""" - - config_sensor = { - CONF_NAME: SENSOR_NAME, - **do_config, - } - caplog.set_level(logging.WARNING) - caplog.clear() - - await base_config_test( - hass, - config_sensor, - SENSOR_NAME, - SENSOR_DOMAIN, - CONF_SENSORS, - None, - method_discovery=True, - expect_setup_to_fail=True, - ) - - assert caplog.text.count(error_message) + messages = str([x.message for x in caplog.get_records("setup")]) + assert error_message in messages @pytest.mark.parametrize( From 6ea50823c19227a99ee5d13d8bef8b0487b8a1fb Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 9 Aug 2021 12:16:35 +0200 Subject: [PATCH 063/355] Use SensorEntityDescription for arlo (#54223) * Use SensorEntityDescription. --- homeassistant/components/arlo/sensor.py | 125 +++++++++++++----------- tests/components/arlo/test_sensor.py | 7 +- 2 files changed, 75 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/arlo/sensor.py b/homeassistant/components/arlo/sensor.py index c794bf1ef5e..883fd011c52 100644 --- a/homeassistant/components/arlo/sensor.py +++ b/homeassistant/components/arlo/sensor.py @@ -1,13 +1,19 @@ """Sensor support for Netgear Arlo IP cameras.""" +from dataclasses import replace import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( ATTR_ATTRIBUTION, CONCENTRATION_PARTS_PER_MILLION, CONF_MONITORED_CONDITIONS, + DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, @@ -22,17 +28,43 @@ from . import ATTRIBUTION, DATA_ARLO, DEFAULT_BRAND, SIGNAL_UPDATE_ARLO _LOGGER = logging.getLogger(__name__) -# sensor_type [ description, unit, icon ] -SENSOR_TYPES = { - "last_capture": ["Last", None, "run-fast"], - "total_cameras": ["Arlo Cameras", None, "video"], - "captured_today": ["Captured Today", None, "file-video"], - "battery_level": ["Battery Level", PERCENTAGE, "battery-50"], - "signal_strength": ["Signal Strength", None, "signal"], - "temperature": ["Temperature", TEMP_CELSIUS, "thermometer"], - "humidity": ["Humidity", PERCENTAGE, "water-percent"], - "air_quality": ["Air Quality", CONCENTRATION_PARTS_PER_MILLION, "biohazard"], -} +SENSOR_TYPES = ( + SensorEntityDescription(key="last_capture", name="Last", icon="mdi:run-fast"), + SensorEntityDescription(key="total_cameras", name="Arlo Cameras", icon="mdi:video"), + SensorEntityDescription( + key="captured_today", name="Captured Today", icon="mdi:file-video" + ), + SensorEntityDescription( + key="battery_level", + name="Battery Level", + unit_of_measurement=PERCENTAGE, + icon="mdi:battery-50", + device_class=DEVICE_CLASS_BATTERY, + ), + SensorEntityDescription( + key="signal_strength", name="Signal Strength", icon="mdi:signal" + ), + SensorEntityDescription( + key="temperature", + name="Temperature", + unit_of_measurement=TEMP_CELSIUS, + icon="mdi:thermometer", + device_class=DEVICE_CLASS_TEMPERATURE, + ), + SensorEntityDescription( + key="humidity", + name="Humidity", + unit_of_measurement=PERCENTAGE, + icon="mdi:water-percent", + device_class=DEVICE_CLASS_HUMIDITY, + ), + SensorEntityDescription( + key="air_quality", + name="Air Quality", + unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + icon="mdi:biohazard", + ), +) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -50,24 +82,27 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return sensors = [] - for sensor_type in config[CONF_MONITORED_CONDITIONS]: - if sensor_type == "total_cameras": - sensors.append(ArloSensor(SENSOR_TYPES[sensor_type][0], arlo, sensor_type)) + for sensor_original in SENSOR_TYPES: + if sensor_original.key not in config[CONF_MONITORED_CONDITIONS]: + continue + sensor_entry = replace(sensor_original) + if sensor_entry.key == "total_cameras": + sensors.append(ArloSensor(arlo, sensor_entry)) else: for camera in arlo.cameras: - if sensor_type in ("temperature", "humidity", "air_quality"): + if sensor_entry.key in ("temperature", "humidity", "air_quality"): continue - name = f"{SENSOR_TYPES[sensor_type][0]} {camera.name}" - sensors.append(ArloSensor(name, camera, sensor_type)) + sensor_entry.name = f"{sensor_entry.name} {camera.name}" + sensors.append(ArloSensor(camera, sensor_entry)) for base_station in arlo.base_stations: if ( - sensor_type in ("temperature", "humidity", "air_quality") + sensor_entry.key in ("temperature", "humidity", "air_quality") and base_station.model_id == "ABC1000" ): - name = f"{SENSOR_TYPES[sensor_type][0]} {base_station.name}" - sensors.append(ArloSensor(name, base_station, sensor_type)) + sensor_entry.name = f"{sensor_entry.name} {base_station.name}" + sensors.append(ArloSensor(base_station, sensor_entry)) add_entities(sensors, True) @@ -75,19 +110,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class ArloSensor(SensorEntity): """An implementation of a Netgear Arlo IP sensor.""" - def __init__(self, name, device, sensor_type): + def __init__(self, device, sensor_entry): """Initialize an Arlo sensor.""" - _LOGGER.debug("ArloSensor created for %s", name) - self._name = name + self.entity_description = sensor_entry self._data = device - self._sensor_type = sensor_type self._state = None - self._icon = f"mdi:{SENSOR_TYPES.get(self._sensor_type)[2]}" - - @property - def name(self): - """Return the name of this camera.""" - return self._name async def async_added_to_hass(self): """Register callbacks.""" @@ -110,36 +137,22 @@ class ArloSensor(SensorEntity): @property def icon(self): """Icon to use in the frontend, if any.""" - if self._sensor_type == "battery_level" and self._state is not None: + if self.entity_description.key == "battery_level" and self._state is not None: return icon_for_battery_level( battery_level=int(self._state), charging=False ) - return self._icon - - @property - def unit_of_measurement(self): - """Return the units of measurement.""" - return SENSOR_TYPES.get(self._sensor_type)[1] - - @property - def device_class(self): - """Return the device class of the sensor.""" - if self._sensor_type == "temperature": - return DEVICE_CLASS_TEMPERATURE - if self._sensor_type == "humidity": - return DEVICE_CLASS_HUMIDITY - return None + return self.entity_description.icon def update(self): """Get the latest data and updates the state.""" _LOGGER.debug("Updating Arlo sensor %s", self.name) - if self._sensor_type == "total_cameras": + if self.entity_description.key == "total_cameras": self._state = len(self._data.cameras) - elif self._sensor_type == "captured_today": + elif self.entity_description.key == "captured_today": self._state = len(self._data.captured_today) - elif self._sensor_type == "last_capture": + elif self.entity_description.key == "last_capture": try: video = self._data.last_video self._state = video.created_at_pretty("%m-%d-%Y %H:%M:%S") @@ -151,31 +164,31 @@ class ArloSensor(SensorEntity): _LOGGER.debug(error_msg) self._state = None - elif self._sensor_type == "battery_level": + elif self.entity_description.key == "battery_level": try: self._state = self._data.battery_level except TypeError: self._state = None - elif self._sensor_type == "signal_strength": + elif self.entity_description.key == "signal_strength": try: self._state = self._data.signal_strength except TypeError: self._state = None - elif self._sensor_type == "temperature": + elif self.entity_description.key == "temperature": try: self._state = self._data.ambient_temperature except TypeError: self._state = None - elif self._sensor_type == "humidity": + elif self.entity_description.key == "humidity": try: self._state = self._data.ambient_humidity except TypeError: self._state = None - elif self._sensor_type == "air_quality": + elif self.entity_description.key == "air_quality": try: self._state = self._data.ambient_air_quality except TypeError: @@ -189,7 +202,7 @@ class ArloSensor(SensorEntity): attrs[ATTR_ATTRIBUTION] = ATTRIBUTION attrs["brand"] = DEFAULT_BRAND - if self._sensor_type != "total_cameras": + if self.entity_description.key != "total_cameras": attrs["model"] = self._data.model_id return attrs diff --git a/tests/components/arlo/test_sensor.py b/tests/components/arlo/test_sensor.py index b8389d1903f..34d5088397d 100644 --- a/tests/components/arlo/test_sensor.py +++ b/tests/components/arlo/test_sensor.py @@ -5,6 +5,7 @@ from unittest.mock import patch import pytest from homeassistant.components.arlo import DATA_ARLO, sensor as arlo +from homeassistant.components.arlo.sensor import SENSOR_TYPES from homeassistant.const import ( ATTR_ATTRIBUTION, DEVICE_CLASS_HUMIDITY, @@ -20,7 +21,11 @@ def _get_named_tuple(input_dict): def _get_sensor(name="Last", sensor_type="last_capture", data=None): if data is None: data = {} - return arlo.ArloSensor(name, data, sensor_type) + sensor_entry = next( + sensor_entry for sensor_entry in SENSOR_TYPES if sensor_entry.key == sensor_type + ) + sensor_entry.name = name + return arlo.ArloSensor(data, sensor_entry) @pytest.fixture() From 608f406a2ca961a3e9829dab51b8785796652915 Mon Sep 17 00:00:00 2001 From: cpw Date: Mon, 9 Aug 2021 06:38:05 -0400 Subject: [PATCH 064/355] Update services.yaml for matrix service to fix Data field being replaced by [object Object] in UI (#54296) --- homeassistant/components/matrix/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/matrix/services.yaml b/homeassistant/components/matrix/services.yaml index 66988def22d..c58a27c3370 100644 --- a/homeassistant/components/matrix/services.yaml +++ b/homeassistant/components/matrix/services.yaml @@ -21,4 +21,4 @@ send_message: description: Extended information of notification. Supports list of images. Optional. example: "{'images': ['/tmp/test.jpg']}" selector: - text: + object: From 9b7b787fe41f0ec4876581d1d985b54923d19bb8 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 9 Aug 2021 13:13:11 +0200 Subject: [PATCH 065/355] Remove icon where device_class is defined. (#54323) --- homeassistant/components/arlo/sensor.py | 3 --- tests/components/arlo/test_sensor.py | 13 +++++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/arlo/sensor.py b/homeassistant/components/arlo/sensor.py index 883fd011c52..e78c8b7bf49 100644 --- a/homeassistant/components/arlo/sensor.py +++ b/homeassistant/components/arlo/sensor.py @@ -38,7 +38,6 @@ SENSOR_TYPES = ( key="battery_level", name="Battery Level", unit_of_measurement=PERCENTAGE, - icon="mdi:battery-50", device_class=DEVICE_CLASS_BATTERY, ), SensorEntityDescription( @@ -48,14 +47,12 @@ SENSOR_TYPES = ( key="temperature", name="Temperature", unit_of_measurement=TEMP_CELSIUS, - icon="mdi:thermometer", device_class=DEVICE_CLASS_TEMPERATURE, ), SensorEntityDescription( key="humidity", name="Humidity", unit_of_measurement=PERCENTAGE, - icon="mdi:water-percent", device_class=DEVICE_CLASS_HUMIDITY, ), SensorEntityDescription( diff --git a/tests/components/arlo/test_sensor.py b/tests/components/arlo/test_sensor.py index 34d5088397d..bf67ab21c97 100644 --- a/tests/components/arlo/test_sensor.py +++ b/tests/components/arlo/test_sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.arlo import DATA_ARLO, sensor as arlo from homeassistant.components.arlo.sensor import SENSOR_TYPES from homeassistant.const import ( ATTR_ATTRIBUTION, + DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, @@ -161,14 +162,14 @@ def test_sensor_state_default(default_sensor): assert default_sensor.state is None -def test_sensor_icon_battery(battery_sensor): - """Test the battery icon.""" - assert battery_sensor.icon == "mdi:battery-50" +def test_sensor_device_class__battery(battery_sensor): + """Test the battery device_class.""" + assert battery_sensor.device_class == DEVICE_CLASS_BATTERY -def test_sensor_icon(temperature_sensor): - """Test the icon property.""" - assert temperature_sensor.icon == "mdi:thermometer" +def test_sensor_device_class(temperature_sensor): + """Test the device_class property.""" + assert temperature_sensor.device_class == DEVICE_CLASS_TEMPERATURE def test_unit_of_measure(default_sensor, battery_sensor): From 3742333a8943503417dff7ea514dc2314dff1d64 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 9 Aug 2021 04:21:41 -0700 Subject: [PATCH 066/355] Remove zwave_js transition on individual color channels (#54303) --- homeassistant/components/zwave_js/light.py | 2 +- tests/components/zwave_js/test_services.py | 9 +++++---- .../zwave_js/bulb_6_multi_color_state.json | 15 +++++---------- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index f3cabe8b6a7..4f1de6c686d 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -301,7 +301,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): # fallback to setting the color(s) one by one if multicolor fails # not sure this is needed at all, but just in case for color, value in colors.items(): - await self._async_set_color(color, value, zwave_transition) + await self._async_set_color(color, value) async def _async_set_color( self, diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 3ee656e40c0..4cc5b599f19 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -1021,8 +1021,7 @@ async def test_multicast_set_value_options( ], ATTR_COMMAND_CLASS: 51, ATTR_PROPERTY: "targetColor", - ATTR_PROPERTY_KEY: 2, - ATTR_VALUE: 2, + ATTR_VALUE: '{ "warmWhite": 0, "coldWhite": 0, "red": 255, "green": 0, "blue": 0 }', ATTR_OPTIONS: {"transitionDuration": 1}, }, blocking=True, @@ -1038,9 +1037,11 @@ async def test_multicast_set_value_options( assert args["valueId"] == { "commandClass": 51, "property": "targetColor", - "propertyKey": 2, } - assert args["value"] == 2 + assert ( + args["value"] + == '{ "warmWhite": 0, "coldWhite": 0, "red": 255, "green": 0, "blue": 0 }' + ) assert args["options"] == {"transitionDuration": 1} client.async_send_command.reset_mock() diff --git a/tests/fixtures/zwave_js/bulb_6_multi_color_state.json b/tests/fixtures/zwave_js/bulb_6_multi_color_state.json index dfa72af6aa4..58608131e90 100644 --- a/tests/fixtures/zwave_js/bulb_6_multi_color_state.json +++ b/tests/fixtures/zwave_js/bulb_6_multi_color_state.json @@ -267,8 +267,7 @@ "min": 0, "max": 255, "label": "Target value (Warm White)", - "description": "The target value of the Warm White color.", - "valueChangeOptions": ["transitionDuration"] + "description": "The target value of the Warm White color." } }, { @@ -286,8 +285,7 @@ "min": 0, "max": 255, "label": "Target value (Cold White)", - "description": "The target value of the Cold White color.", - "valueChangeOptions": ["transitionDuration"] + "description": "The target value of the Cold White color." } }, { @@ -305,8 +303,7 @@ "min": 0, "max": 255, "label": "Target value (Red)", - "description": "The target value of the Red color.", - "valueChangeOptions": ["transitionDuration"] + "description": "The target value of the Red color." } }, { @@ -324,8 +321,7 @@ "min": 0, "max": 255, "label": "Target value (Green)", - "description": "The target value of the Green color.", - "valueChangeOptions": ["transitionDuration"] + "description": "The target value of the Green color." } }, { @@ -343,8 +339,7 @@ "min": 0, "max": 255, "label": "Target value (Blue)", - "description": "The target value of the Blue color.", - "valueChangeOptions": ["transitionDuration"] + "description": "The target value of the Blue color." } }, { From c79ee53ab121dda1540b2a4814a1533ca87ca8e1 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 9 Aug 2021 07:29:17 -0400 Subject: [PATCH 067/355] Use dict for zwave_js siren.available_tones (#54305) * Use dict for zwave_js siren.available_tones * update siren.turn_on service description --- homeassistant/components/siren/services.yaml | 2 +- homeassistant/components/zwave_js/siren.py | 18 ++----- tests/components/zwave_js/test_siren.py | 56 ++++++++++++++++++++ 3 files changed, 62 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/siren/services.yaml b/homeassistant/components/siren/services.yaml index 8c5ed3be974..18bf782eaf2 100644 --- a/homeassistant/components/siren/services.yaml +++ b/homeassistant/components/siren/services.yaml @@ -7,7 +7,7 @@ turn_on: domain: siren fields: tone: - description: The tone to emit when turning the siren on. Must be supported by the integration. + description: The tone to emit when turning the siren on. When `available_tones` property is a map, either the key or the value can be used. Must be supported by the integration. example: fire required: false selector: diff --git a/homeassistant/components/zwave_js/siren.py b/homeassistant/components/zwave_js/siren.py index de74f55fa9a..c1b354f4faa 100644 --- a/homeassistant/components/zwave_js/siren.py +++ b/homeassistant/components/zwave_js/siren.py @@ -58,9 +58,9 @@ class ZwaveSirenEntity(ZWaveBaseEntity, SirenEntity): """Initialize a ZwaveSirenEntity entity.""" super().__init__(config_entry, client, info) # Entity class attributes - self._attr_available_tones = list( - self.info.primary_value.metadata.states.values() - ) + self._attr_available_tones = { + int(id): val for id, val in self.info.primary_value.metadata.states.items() + } self._attr_supported_features = ( SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_VOLUME_SET ) @@ -82,23 +82,15 @@ class ZwaveSirenEntity(ZWaveBaseEntity, SirenEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" - tone: str | None = kwargs.get(ATTR_TONE) + tone_id: int | None = kwargs.get(ATTR_TONE) options = {} if (volume := kwargs.get(ATTR_VOLUME_LEVEL)) is not None: options["volume"] = round(volume * 100) # Play the default tone if a tone isn't provided - if tone is None: + if tone_id is None: await self.async_set_value(ToneID.DEFAULT, options) return - tone_id = int( - next( - key - for key, value in self.info.primary_value.metadata.states.items() - if value == tone - ) - ) - await self.async_set_value(tone_id, options) async def async_turn_off(self, **kwargs: Any) -> None: diff --git a/tests/components/zwave_js/test_siren.py b/tests/components/zwave_js/test_siren.py index 937b2c0fa67..ebe437eb981 100644 --- a/tests/components/zwave_js/test_siren.py +++ b/tests/components/zwave_js/test_siren.py @@ -2,6 +2,7 @@ from zwave_js_server.event import Event from homeassistant.components.siren import ATTR_TONE, ATTR_VOLUME_LEVEL +from homeassistant.components.siren.const import ATTR_AVAILABLE_TONES from homeassistant.const import STATE_OFF, STATE_ON SIREN_ENTITY = "siren.indoor_siren_6_2" @@ -65,6 +66,39 @@ async def test_siren(hass, client, aeotec_zw164_siren, integration): assert state assert state.state == STATE_OFF + assert state.attributes.get(ATTR_AVAILABLE_TONES) == { + 0: "off", + 1: "01DING~1 (5 sec)", + 2: "02DING~1 (9 sec)", + 3: "03TRAD~1 (11 sec)", + 4: "04ELEC~1 (2 sec)", + 5: "05WEST~1 (13 sec)", + 6: "06CHIM~1 (7 sec)", + 7: "07CUCK~1 (31 sec)", + 8: "08TRAD~1 (6 sec)", + 9: "09SMOK~1 (11 sec)", + 10: "10SMOK~1 (6 sec)", + 11: "11FIRE~1 (35 sec)", + 12: "12COSE~1 (5 sec)", + 13: "13KLAX~1 (38 sec)", + 14: "14DEEP~1 (41 sec)", + 15: "15WARN~1 (37 sec)", + 16: "16TORN~1 (46 sec)", + 17: "17ALAR~1 (35 sec)", + 18: "18DEEP~1 (62 sec)", + 19: "19ALAR~1 (15 sec)", + 20: "20ALAR~1 (7 sec)", + 21: "21DIGI~1 (8 sec)", + 22: "22ALER~1 (64 sec)", + 23: "23SHIP~1 (4 sec)", + 25: "25CHRI~1 (4 sec)", + 26: "26GONG~1 (12 sec)", + 27: "27SING~1 (1 sec)", + 28: "28TONA~1 (5 sec)", + 29: "29UPWA~1 (2 sec)", + 30: "30DOOR~1 (27 sec)", + 255: "default", + } # Test turn on with default await hass.services.async_call( @@ -105,6 +139,28 @@ async def test_siren(hass, client, aeotec_zw164_siren, integration): client.async_send_command.reset_mock() + # Test turn on with specific tone ID and volume level + await hass.services.async_call( + "siren", + "turn_on", + { + "entity_id": SIREN_ENTITY, + ATTR_TONE: 1, + ATTR_VOLUME_LEVEL: 0.5, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == node.node_id + assert args["valueId"] == TONE_ID_VALUE_ID + assert args["value"] == 1 + assert args["options"] == {"volume": 50} + + client.async_send_command.reset_mock() + # Test turn off await hass.services.async_call( "siren", From 1c7891fbee392b09c845e6c9fe9605c92fa371a0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 9 Aug 2021 14:54:42 +0200 Subject: [PATCH 068/355] Remove deprecated YAML configuration from Growatt (#54325) --- .../components/growatt_server/config_flow.py | 4 --- .../components/growatt_server/sensor.py | 31 ++----------------- .../growatt_server/test_config_flow.py | 24 -------------- 3 files changed, 2 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/growatt_server/config_flow.py b/homeassistant/components/growatt_server/config_flow.py index 45f56a327b2..d6b2c7db9fe 100644 --- a/homeassistant/components/growatt_server/config_flow.py +++ b/homeassistant/components/growatt_server/config_flow.py @@ -76,7 +76,3 @@ class GrowattServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() self.data.update(user_input) return self.async_create_entry(title=self.data[CONF_NAME], data=self.data) - - async def async_step_import(self, import_data): - """Migrate old yaml config to config flow.""" - return await self.async_step_user(import_data) diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index fe6bdeb70e8..530842fab7b 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -5,10 +5,8 @@ import logging import re import growattServer -import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, @@ -31,10 +29,9 @@ from homeassistant.const import ( POWER_WATT, TEMP_CELSIUS, ) -import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle, dt -from .const import CONF_PLANT_ID, DEFAULT_NAME, DEFAULT_PLANT_ID, DEFAULT_URL, DOMAIN +from .const import CONF_PLANT_ID, DEFAULT_PLANT_ID, DEFAULT_URL _LOGGER = logging.getLogger(__name__) @@ -549,30 +546,6 @@ SENSOR_TYPES = { **MIX_SENSOR_TYPES, } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PLANT_ID, default=DEFAULT_PLANT_ID): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_URL, default=DEFAULT_URL): cv.string, - } -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up growatt server from yaml.""" - if not hass.config_entries.async_entries(DOMAIN): - _LOGGER.warning( - "Loading Growatt 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 - ) - ) - def get_device_list(api, config): """Retrieve the device list for the selected plant.""" diff --git a/tests/components/growatt_server/test_config_flow.py b/tests/components/growatt_server/test_config_flow.py index 662448c8118..096052fd6cf 100644 --- a/tests/components/growatt_server/test_config_flow.py +++ b/tests/components/growatt_server/test_config_flow.py @@ -149,30 +149,6 @@ async def test_one_plant_on_account(hass): assert result["data"][CONF_PLANT_ID] == "123456" -async def test_import_one_plant(hass): - """Test import step with a single plant.""" - import_data = FIXTURE_USER_INPUT.copy() - - with patch( - "growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE - ), patch( - "growattServer.GrowattApi.plant_list", - return_value=GROWATT_PLANT_LIST_RESPONSE, - ), patch( - "homeassistant.components.growatt_server.async_setup_entry", return_value=True - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=import_data, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] - assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] - assert result["data"][CONF_PLANT_ID] == "123456" - - async def test_existing_plant_configured(hass): """Test entering an existing plant_id.""" entry = MockConfigEntry(domain=DOMAIN, unique_id="123456") From 188919f079b1727d74e6fc904f9e807c48653e01 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 9 Aug 2021 09:45:12 -0700 Subject: [PATCH 069/355] Clean up zwave_js RGB code (#54336) --- homeassistant/components/zwave_js/light.py | 37 +---- tests/components/zwave_js/test_light.py | 149 ++++++--------------- 2 files changed, 46 insertions(+), 140 deletions(-) diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index 4f1de6c686d..91a7f191e5d 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -287,39 +287,14 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): else: zwave_transition = {TRANSITION_DURATION: "default"} - if combined_color_val and isinstance(combined_color_val.value, dict): - colors_dict = {} - for color, value in colors.items(): - color_name = MULTI_COLOR_MAP[color] - colors_dict[color_name] = value - # set updated color object - await self.info.node.async_set_value( - combined_color_val, colors_dict, zwave_transition - ) - return - - # fallback to setting the color(s) one by one if multicolor fails - # not sure this is needed at all, but just in case + colors_dict = {} for color, value in colors.items(): - await self._async_set_color(color, value) - - async def _async_set_color( - self, - color: ColorComponent, - new_value: int, - transition: dict[str, str] | None = None, - ) -> None: - """Set defined color to given value.""" - # actually set the new color value - target_zwave_value = self.get_zwave_value( - "targetColor", - CommandClass.SWITCH_COLOR, - value_property_key=color.value, + color_name = MULTI_COLOR_MAP[color] + colors_dict[color_name] = value + # set updated color object + await self.info.node.async_set_value( + combined_color_val, colors_dict, zwave_transition ) - if target_zwave_value is None: - # guard for unsupported color - return - await self.info.node.async_set_value(target_zwave_value, new_value, transition) async def _async_set_brightness( self, brightness: int | None, transition: float | None = None diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py index fa3c73a9a42..5ce66d6d8e2 100644 --- a/tests/components/zwave_js/test_light.py +++ b/tests/components/zwave_js/test_light.py @@ -223,58 +223,23 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 6 - warm_args = client.async_send_command.call_args_list[0][0][0] # red 255 - assert warm_args["command"] == "node.set_value" - assert warm_args["nodeId"] == 39 - assert warm_args["valueId"]["commandClassName"] == "Color Switch" - assert warm_args["valueId"]["commandClass"] == 51 - assert warm_args["valueId"]["endpoint"] == 0 - assert warm_args["valueId"]["metadata"]["label"] == "Target value (Red)" - assert warm_args["valueId"]["property"] == "targetColor" - assert warm_args["valueId"]["propertyName"] == "targetColor" - assert warm_args["value"] == 255 - - cold_args = client.async_send_command.call_args_list[1][0][0] # green 76 - assert cold_args["command"] == "node.set_value" - assert cold_args["nodeId"] == 39 - assert cold_args["valueId"]["commandClassName"] == "Color Switch" - assert cold_args["valueId"]["commandClass"] == 51 - assert cold_args["valueId"]["endpoint"] == 0 - assert cold_args["valueId"]["metadata"]["label"] == "Target value (Green)" - assert cold_args["valueId"]["property"] == "targetColor" - assert cold_args["valueId"]["propertyName"] == "targetColor" - assert cold_args["value"] == 76 - red_args = client.async_send_command.call_args_list[2][0][0] # blue 255 - assert red_args["command"] == "node.set_value" - assert red_args["nodeId"] == 39 - assert red_args["valueId"]["commandClassName"] == "Color Switch" - assert red_args["valueId"]["commandClass"] == 51 - assert red_args["valueId"]["endpoint"] == 0 - assert red_args["valueId"]["metadata"]["label"] == "Target value (Blue)" - assert red_args["valueId"]["property"] == "targetColor" - assert red_args["valueId"]["propertyName"] == "targetColor" - assert red_args["value"] == 255 - green_args = client.async_send_command.call_args_list[3][0][0] # warm white 0 - assert green_args["command"] == "node.set_value" - assert green_args["nodeId"] == 39 - assert green_args["valueId"]["commandClassName"] == "Color Switch" - assert green_args["valueId"]["commandClass"] == 51 - assert green_args["valueId"]["endpoint"] == 0 - assert green_args["valueId"]["metadata"]["label"] == "Target value (Warm White)" - assert green_args["valueId"]["property"] == "targetColor" - assert green_args["valueId"]["propertyName"] == "targetColor" - assert green_args["value"] == 0 - blue_args = client.async_send_command.call_args_list[4][0][0] # cold white 0 - assert blue_args["command"] == "node.set_value" - assert blue_args["nodeId"] == 39 - assert blue_args["valueId"]["commandClassName"] == "Color Switch" - assert blue_args["valueId"]["commandClass"] == 51 - assert blue_args["valueId"]["endpoint"] == 0 - assert blue_args["valueId"]["metadata"]["label"] == "Target value (Cold White)" - assert blue_args["valueId"]["property"] == "targetColor" - assert blue_args["valueId"]["propertyName"] == "targetColor" - assert blue_args["value"] == 0 + assert len(client.async_send_command.call_args_list) == 2 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 39 + assert args["valueId"]["commandClassName"] == "Color Switch" + assert args["valueId"]["commandClass"] == 51 + assert args["valueId"]["endpoint"] == 0 + assert args["valueId"]["metadata"]["label"] == "Target Color" + assert args["valueId"]["property"] == "targetColor" + assert args["valueId"]["propertyName"] == "targetColor" + assert args["value"] == { + "blue": 255, + "coldWhite": 0, + "green": 76, + "red": 255, + "warmWhite": 0, + } # Test rgb color update from value updated event red_event = Event( @@ -328,7 +293,7 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 6 + assert len(client.async_send_command.call_args_list) == 2 client.async_send_command.reset_mock() @@ -344,8 +309,8 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 6 - args = client.async_send_command.call_args_list[5][0][0] + assert len(client.async_send_command.call_args_list) == 2 + args = client.async_send_command.call_args_list[0][0][0] assert args["options"]["transitionDuration"] == "20s" client.async_send_command.reset_mock() @@ -357,57 +322,23 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 6 - red_args = client.async_send_command.call_args_list[0][0][0] # red 0 - assert red_args["command"] == "node.set_value" - assert red_args["nodeId"] == 39 - assert red_args["valueId"]["commandClassName"] == "Color Switch" - assert red_args["valueId"]["commandClass"] == 51 - assert red_args["valueId"]["endpoint"] == 0 - assert red_args["valueId"]["metadata"]["label"] == "Target value (Red)" - assert red_args["valueId"]["property"] == "targetColor" - assert red_args["valueId"]["propertyName"] == "targetColor" - assert red_args["value"] == 0 - red_args = client.async_send_command.call_args_list[1][0][0] # green 0 - assert red_args["command"] == "node.set_value" - assert red_args["nodeId"] == 39 - assert red_args["valueId"]["commandClassName"] == "Color Switch" - assert red_args["valueId"]["commandClass"] == 51 - assert red_args["valueId"]["endpoint"] == 0 - assert red_args["valueId"]["metadata"]["label"] == "Target value (Green)" - assert red_args["valueId"]["property"] == "targetColor" - assert red_args["valueId"]["propertyName"] == "targetColor" - assert red_args["value"] == 0 - red_args = client.async_send_command.call_args_list[2][0][0] # blue 0 - assert red_args["command"] == "node.set_value" - assert red_args["nodeId"] == 39 - assert red_args["valueId"]["commandClassName"] == "Color Switch" - assert red_args["valueId"]["commandClass"] == 51 - assert red_args["valueId"]["endpoint"] == 0 - assert red_args["valueId"]["metadata"]["label"] == "Target value (Blue)" - assert red_args["valueId"]["property"] == "targetColor" - assert red_args["valueId"]["propertyName"] == "targetColor" - assert red_args["value"] == 0 - warm_args = client.async_send_command.call_args_list[3][0][0] # warm white 0 - assert warm_args["command"] == "node.set_value" - assert warm_args["nodeId"] == 39 - assert warm_args["valueId"]["commandClassName"] == "Color Switch" - assert warm_args["valueId"]["commandClass"] == 51 - assert warm_args["valueId"]["endpoint"] == 0 - assert warm_args["valueId"]["metadata"]["label"] == "Target value (Warm White)" - assert warm_args["valueId"]["property"] == "targetColor" - assert warm_args["valueId"]["propertyName"] == "targetColor" - assert warm_args["value"] == 20 - red_args = client.async_send_command.call_args_list[4][0][0] # cold white - assert red_args["command"] == "node.set_value" - assert red_args["nodeId"] == 39 - assert red_args["valueId"]["commandClassName"] == "Color Switch" - assert red_args["valueId"]["commandClass"] == 51 - assert red_args["valueId"]["endpoint"] == 0 - assert red_args["valueId"]["metadata"]["label"] == "Target value (Cold White)" - assert red_args["valueId"]["property"] == "targetColor" - assert red_args["valueId"]["propertyName"] == "targetColor" - assert red_args["value"] == 235 + assert len(client.async_send_command.call_args_list) == 2 + args = client.async_send_command.call_args_list[0][0][0] # red 0 + assert args["command"] == "node.set_value" + assert args["nodeId"] == 39 + assert args["valueId"]["commandClassName"] == "Color Switch" + assert args["valueId"]["commandClass"] == 51 + assert args["valueId"]["endpoint"] == 0 + assert args["valueId"]["metadata"]["label"] == "Target Color" + assert args["valueId"]["property"] == "targetColor" + assert args["valueId"]["propertyName"] == "targetColor" + assert args["value"] == { + "blue": 0, + "coldWhite": 235, + "green": 0, + "red": 0, + "warmWhite": 20, + } client.async_send_command.reset_mock() @@ -466,7 +397,7 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 6 + assert len(client.async_send_command.call_args_list) == 2 client.async_send_command.reset_mock() @@ -482,8 +413,8 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 6 - args = client.async_send_command.call_args_list[5][0][0] + assert len(client.async_send_command.call_args_list) == 2 + args = client.async_send_command.call_args_list[0][0][0] assert args["options"]["transitionDuration"] == "35s" client.async_send_command.reset_mock() From b88f0adbe91f61b23080467fd8f928af77d17672 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Mon, 9 Aug 2021 18:48:01 +0100 Subject: [PATCH 070/355] Restores unit_of_measurement (#54335) --- homeassistant/components/integration/sensor.py | 4 ++++ tests/components/integration/test_sensor.py | 1 + 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 2b7d89decea..9308adc622d 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -145,6 +145,10 @@ class IntegrationSensor(RestoreEntity, SensorEntity): ) self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS) + self._unit_of_measurement = state.attributes.get( + ATTR_UNIT_OF_MEASUREMENT + ) + @callback def calc_integration(event): """Handle the sensor state changes.""" diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index dd6bf980d0f..36d3d4b3b30 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -73,6 +73,7 @@ async def test_restore_state(hass: HomeAssistant) -> None: { "last_reset": "2019-10-06T21:00:00", "device_class": DEVICE_CLASS_ENERGY, + "unit_of_measurement": ENERGY_KILO_WATT_HOUR, }, ), ), From acf55f2f3af19a744bc8ac52edda38726c46111d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 9 Aug 2021 19:55:14 +0200 Subject: [PATCH 071/355] Add light transition for Shelly integration (#54327) * Add support for light transition * Limit transition to 5 seconds * Update MODELS_SUPPORTING_LIGHT_TRANSITION list --- homeassistant/components/shelly/const.py | 17 ++++++++++++++ homeassistant/components/shelly/light.py | 29 +++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 49e33dfd5e1..ea6b9320cb1 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -1,6 +1,7 @@ """Constants for the Shelly integration.""" from __future__ import annotations +import re from typing import Final COAP: Final = "coap" @@ -11,6 +12,22 @@ REST: Final = "rest" CONF_COAP_PORT: Final = "coap_port" DEFAULT_COAP_PORT: Final = 5683 +FIRMWARE_PATTERN: Final = re.compile(r"^(\d{8})") + +# Firmware 1.11.0 release date, this firmware supports light transition +LIGHT_TRANSITION_MIN_FIRMWARE_DATE: Final = 20210226 + +# max light transition time in milliseconds +MAX_TRANSITION_TIME: Final = 5000 + +MODELS_SUPPORTING_LIGHT_TRANSITION: Final = ( + "SHBDUO-1", + "SHCB-1", + "SHDM-1", + "SHDM-2", + "SHRGBW2", + "SHVIN-1", +) # Used in "_async_update_data" as timeout for polling data from devices. POLLING_TIMEOUT_SEC: Final = 18 diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 047a105a30f..86624410708 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -14,12 +14,14 @@ from homeassistant.components.light import ( ATTR_EFFECT, ATTR_RGB_COLOR, ATTR_RGBW_COLOR, + ATTR_TRANSITION, COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, COLOR_MODE_ONOFF, COLOR_MODE_RGB, COLOR_MODE_RGBW, SUPPORT_EFFECT, + SUPPORT_TRANSITION, LightEntity, brightness_supported, ) @@ -37,9 +39,13 @@ from .const import ( COAP, DATA_CONFIG_ENTRY, DOMAIN, + FIRMWARE_PATTERN, KELVIN_MAX_VALUE, KELVIN_MIN_VALUE_COLOR, KELVIN_MIN_VALUE_WHITE, + LIGHT_TRANSITION_MIN_FIRMWARE_DATE, + MAX_TRANSITION_TIME, + MODELS_SUPPORTING_LIGHT_TRANSITION, SHBLB_1_RGB_EFFECTS, STANDARD_RGB_EFFECTS, ) @@ -110,6 +116,14 @@ class ShellyLight(ShellyBlockEntity, LightEntity): if hasattr(block, "effect"): self._supported_features |= SUPPORT_EFFECT + if wrapper.model in MODELS_SUPPORTING_LIGHT_TRANSITION: + match = FIRMWARE_PATTERN.search(wrapper.device.settings.get("fw")) + if ( + match is not None + and int(match[0]) >= LIGHT_TRANSITION_MIN_FIRMWARE_DATE + ): + self._supported_features |= SUPPORT_TRANSITION + @property def supported_features(self) -> int: """Supported features.""" @@ -261,6 +275,11 @@ class ShellyLight(ShellyBlockEntity, LightEntity): supported_color_modes = self._supported_color_modes params: dict[str, Any] = {"turn": "on"} + if ATTR_TRANSITION in kwargs: + params["transition"] = min( + int(kwargs[ATTR_TRANSITION] * 1000), MAX_TRANSITION_TIME + ) + if ATTR_BRIGHTNESS in kwargs and brightness_supported(supported_color_modes): brightness_pct = int(100 * (kwargs[ATTR_BRIGHTNESS] + 1) / 255) if hasattr(self.block, "gain"): @@ -312,7 +331,15 @@ class ShellyLight(ShellyBlockEntity, LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn off light.""" - self.control_result = await self.set_state(turn="off") + params: dict[str, Any] = {"turn": "off"} + + if ATTR_TRANSITION in kwargs: + params["transition"] = min( + int(kwargs[ATTR_TRANSITION] * 1000), MAX_TRANSITION_TIME + ) + + self.control_result = await self.set_state(**params) + self.async_write_ha_state() async def set_light_mode(self, set_mode: str | None) -> bool: From a23da30c292c687c35958b3869b4d9b7e84900b9 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 9 Aug 2021 20:33:34 +0200 Subject: [PATCH 072/355] Yeelight local push updates (#51160) Co-authored-by: J. Nick Koston --- CODEOWNERS | 2 +- homeassistant/components/yeelight/__init__.py | 109 +++++---- .../components/yeelight/binary_sensor.py | 1 + homeassistant/components/yeelight/light.py | 222 ++++++++---------- .../components/yeelight/manifest.json | 6 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/yeelight/__init__.py | 24 +- .../components/yeelight/test_binary_sensor.py | 2 +- tests/components/yeelight/test_config_flow.py | 4 +- tests/components/yeelight/test_init.py | 77 ++++-- tests/components/yeelight/test_light.py | 136 ++++++----- 12 files changed, 328 insertions(+), 259 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 86622690fb9..d2e756c0d0d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -584,7 +584,7 @@ homeassistant/components/xmpp/* @fabaff @flowolf homeassistant/components/yale_smart_alarm/* @gjohansson-ST homeassistant/components/yamaha_musiccast/* @vigonotion @micha91 homeassistant/components/yandex_transport/* @rishatik92 @devbis -homeassistant/components/yeelight/* @rytilahti @zewelor @shenxn +homeassistant/components/yeelight/* @rytilahti @zewelor @shenxn @starkillerOG homeassistant/components/yeelightsunflower/* @lindsaymarkward homeassistant/components/yi/* @bachya homeassistant/components/youless/* @gjong diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 2cb754ce6a7..2a4ba4eac55 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -6,7 +6,8 @@ from datetime import timedelta import logging import voluptuous as vol -from yeelight import Bulb, BulbException, discover_bulbs +from yeelight import BulbException, discover_bulbs +from yeelight.aio import KEY_CONNECTED, AsyncBulb from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryNotReady from homeassistant.const import ( @@ -14,13 +15,15 @@ from homeassistant.const import ( CONF_HOST, CONF_ID, CONF_NAME, - CONF_SCAN_INTERVAL, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import DeviceInfo, Entity -from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) @@ -46,7 +49,6 @@ CONF_NIGHTLIGHT_SWITCH = "nightlight_switch" DATA_CONFIG_ENTRIES = "config_entries" DATA_CUSTOM_EFFECTS = "custom_effects" -DATA_SCAN_INTERVAL = "scan_interval" DATA_DEVICE = "device" DATA_REMOVE_INIT_DISPATCHER = "remove_init_dispatcher" DATA_PLATFORMS_LOADED = "platforms_loaded" @@ -65,7 +67,6 @@ ACTIVE_COLOR_FLOWING = "1" NIGHTLIGHT_SWITCH_TYPE_LIGHT = "light" -SCAN_INTERVAL = timedelta(seconds=30) DISCOVERY_INTERVAL = timedelta(seconds=60) YEELIGHT_RGB_TRANSITION = "RGBTransition" @@ -114,7 +115,6 @@ CONFIG_SCHEMA = vol.Schema( DOMAIN: vol.Schema( { vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, vol.Optional(CONF_CUSTOM_EFFECTS): [ { vol.Required(CONF_NAME): cv.string, @@ -158,7 +158,6 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: hass.data[DOMAIN] = { DATA_CUSTOM_EFFECTS: conf.get(CONF_CUSTOM_EFFECTS, {}), DATA_CONFIG_ENTRIES: {}, - DATA_SCAN_INTERVAL: conf.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL), } # Import manually configured devices @@ -196,14 +195,25 @@ async def _async_initialize( device = await _async_get_device(hass, host, entry) entry_data[DATA_DEVICE] = device + # start listening for local pushes + await device.bulb.async_listen(device.async_update_callback) + + # register stop callback to shutdown listening for local pushes + async def async_stop_listen_task(event): + """Stop listen thread.""" + _LOGGER.debug("Shutting down Yeelight Listener") + await device.bulb.async_stop_listening() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_listen_task) + entry.async_on_unload( async_dispatcher_connect( hass, DEVICE_INITIALIZED.format(host), _async_load_platforms ) ) - entry.async_on_unload(device.async_unload) - await device.async_setup() + # fetch initial state + asyncio.create_task(device.async_update()) @callback @@ -248,14 +258,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Otherwise fall through to discovery else: # manually added device - await _async_initialize(hass, entry, entry.data[CONF_HOST], device=device) + try: + await _async_initialize( + hass, entry, entry.data[CONF_HOST], device=device + ) + except BulbException as ex: + raise ConfigEntryNotReady from ex return True # discovery scanner = YeelightScanner.async_get(hass) async def _async_from_discovery(host: str) -> None: - await _async_initialize(hass, entry, host) + try: + await _async_initialize(hass, entry, host) + except BulbException: + _LOGGER.exception("Failed to connect to bulb at %s", host) scanner.async_register_callback(entry.data[CONF_ID], _async_from_discovery) return True @@ -275,6 +293,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: scanner = YeelightScanner.async_get(hass) scanner.async_unregister_callback(entry.data[CONF_ID]) + device = entry_data[DATA_DEVICE] + _LOGGER.debug("Shutting down Yeelight Listener") + await device.bulb.async_stop_listening() + _LOGGER.debug("Yeelight Listener stopped") + data_config_entries.pop(entry.entry_id) return True @@ -331,7 +354,7 @@ class YeelightScanner: if len(self._callbacks) == 0: self._async_stop_scan() - await asyncio.sleep(SCAN_INTERVAL.total_seconds()) + await asyncio.sleep(DISCOVERY_INTERVAL.total_seconds()) self._scan_task = self._hass.loop.create_task(self._async_scan()) @callback @@ -382,7 +405,6 @@ class YeelightDevice: self._capabilities = capabilities or {} self._device_type = None self._available = False - self._remove_time_tracker = None self._initialized = False self._name = host # Default name is host @@ -478,34 +500,36 @@ class YeelightDevice: return self._device_type - def turn_on(self, duration=DEFAULT_TRANSITION, light_type=None, power_mode=None): + async def async_turn_on( + self, duration=DEFAULT_TRANSITION, light_type=None, power_mode=None + ): """Turn on device.""" try: - self.bulb.turn_on( + await self.bulb.async_turn_on( duration=duration, light_type=light_type, power_mode=power_mode ) except BulbException as ex: _LOGGER.error("Unable to turn the bulb on: %s", ex) - def turn_off(self, duration=DEFAULT_TRANSITION, light_type=None): + async def async_turn_off(self, duration=DEFAULT_TRANSITION, light_type=None): """Turn off device.""" try: - self.bulb.turn_off(duration=duration, light_type=light_type) + await self.bulb.async_turn_off(duration=duration, light_type=light_type) except BulbException as ex: _LOGGER.error( "Unable to turn the bulb off: %s, %s: %s", self._host, self.name, ex ) - def _update_properties(self): + async def _async_update_properties(self): """Read new properties from the device.""" if not self.bulb: return try: - self.bulb.get_properties(UPDATE_REQUEST_PROPERTIES) + await self.bulb.async_get_properties(UPDATE_REQUEST_PROPERTIES) self._available = True if not self._initialized: - self._initialize_device() + await self._async_initialize_device() except BulbException as ex: if self._available: # just inform once _LOGGER.error( @@ -515,10 +539,10 @@ class YeelightDevice: return self._available - def _get_capabilities(self): + async def _async_get_capabilities(self): """Request device capabilities.""" try: - self.bulb.get_capabilities() + await self._hass.async_add_executor_job(self.bulb.get_capabilities) _LOGGER.debug( "Device %s, %s capabilities: %s", self._host, @@ -533,31 +557,24 @@ class YeelightDevice: ex, ) - def _initialize_device(self): - self._get_capabilities() + async def _async_initialize_device(self): + await self._async_get_capabilities() self._initialized = True - dispatcher_send(self._hass, DEVICE_INITIALIZED.format(self._host)) + async_dispatcher_send(self._hass, DEVICE_INITIALIZED.format(self._host)) - def update(self): + async def async_update(self): """Update device properties and send data updated signal.""" - self._update_properties() - dispatcher_send(self._hass, DATA_UPDATED.format(self._host)) - - async def async_setup(self): - """Set up the device.""" - - async def _async_update(_): - await self._hass.async_add_executor_job(self.update) - - await _async_update(None) - self._remove_time_tracker = async_track_time_interval( - self._hass, _async_update, self._hass.data[DOMAIN][DATA_SCAN_INTERVAL] - ) + if self._initialized and self._available: + # No need to poll, already connected + return + await self._async_update_properties() + async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host)) @callback - def async_unload(self): - """Unload the device.""" - self._remove_time_tracker() + def async_update_callback(self, data): + """Update push from device.""" + self._available = data.get(KEY_CONNECTED, True) + async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host)) class YeelightEntity(Entity): @@ -597,9 +614,9 @@ class YeelightEntity(Entity): """No polling needed.""" return False - def update(self) -> None: + async def async_update(self) -> None: """Update the entity.""" - self._device.update() + await self._device.async_update() async def _async_get_device( @@ -609,7 +626,7 @@ async def _async_get_device( model = entry.options.get(CONF_MODEL) # Set up device - bulb = Bulb(host, model=model or None) + bulb = AsyncBulb(host, model=model or None) capabilities = await hass.async_add_executor_job(bulb.get_capabilities) return YeelightDevice(hass, host, entry.options, bulb, capabilities) diff --git a/homeassistant/components/yeelight/binary_sensor.py b/homeassistant/components/yeelight/binary_sensor.py index 4fe3709cdd2..185bb504a1b 100644 --- a/homeassistant/components/yeelight/binary_sensor.py +++ b/homeassistant/components/yeelight/binary_sensor.py @@ -33,6 +33,7 @@ class YeelightNightlightModeSensor(YeelightEntity, BinarySensorEntity): self.async_write_ha_state, ) ) + await super().async_added_to_hass() @property def unique_id(self) -> str: diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 8d0a3b0ffd4..d2ddc92bb8d 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -1,7 +1,6 @@ """Light platform support for yeelight.""" from __future__ import annotations -from functools import partial import logging import voluptuous as vol @@ -234,17 +233,17 @@ def _parse_custom_effects(effects_config): return effects -def _cmd(func): +def _async_cmd(func): """Define a wrapper to catch exceptions from the bulb.""" - def _wrap(self, *args, **kwargs): + async def _async_wrap(self, *args, **kwargs): try: _LOGGER.debug("Calling %s with %s %s", func, args, kwargs) - return func(self, *args, **kwargs) + return await func(self, *args, **kwargs) except BulbException as ex: _LOGGER.error("Error when calling %s: %s", func, ex) - return _wrap + return _async_wrap async def async_setup_entry( @@ -306,36 +305,27 @@ def _async_setup_services(hass: HomeAssistant): params = {**service_call.data} params.pop(ATTR_ENTITY_ID) params[ATTR_TRANSITIONS] = _transitions_config_parser(params[ATTR_TRANSITIONS]) - await hass.async_add_executor_job(partial(entity.start_flow, **params)) + await entity.async_start_flow(**params) async def _async_set_color_scene(entity, service_call): - await hass.async_add_executor_job( - partial( - entity.set_scene, - SceneClass.COLOR, - *service_call.data[ATTR_RGB_COLOR], - service_call.data[ATTR_BRIGHTNESS], - ) + await entity.async_set_scene( + SceneClass.COLOR, + *service_call.data[ATTR_RGB_COLOR], + service_call.data[ATTR_BRIGHTNESS], ) async def _async_set_hsv_scene(entity, service_call): - await hass.async_add_executor_job( - partial( - entity.set_scene, - SceneClass.HSV, - *service_call.data[ATTR_HS_COLOR], - service_call.data[ATTR_BRIGHTNESS], - ) + await entity.async_set_scene( + SceneClass.HSV, + *service_call.data[ATTR_HS_COLOR], + service_call.data[ATTR_BRIGHTNESS], ) async def _async_set_color_temp_scene(entity, service_call): - await hass.async_add_executor_job( - partial( - entity.set_scene, - SceneClass.CT, - service_call.data[ATTR_KELVIN], - service_call.data[ATTR_BRIGHTNESS], - ) + await entity.async_set_scene( + SceneClass.CT, + service_call.data[ATTR_KELVIN], + service_call.data[ATTR_BRIGHTNESS], ) async def _async_set_color_flow_scene(entity, service_call): @@ -344,24 +334,19 @@ def _async_setup_services(hass: HomeAssistant): action=Flow.actions[service_call.data[ATTR_ACTION]], transitions=_transitions_config_parser(service_call.data[ATTR_TRANSITIONS]), ) - await hass.async_add_executor_job( - partial(entity.set_scene, SceneClass.CF, flow) - ) + await entity.async_set_scene(SceneClass.CF, flow) async def _async_set_auto_delay_off_scene(entity, service_call): - await hass.async_add_executor_job( - partial( - entity.set_scene, - SceneClass.AUTO_DELAY_OFF, - service_call.data[ATTR_BRIGHTNESS], - service_call.data[ATTR_MINUTES], - ) + await entity.async_set_scene( + SceneClass.AUTO_DELAY_OFF, + service_call.data[ATTR_BRIGHTNESS], + service_call.data[ATTR_MINUTES], ) platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( - SERVICE_SET_MODE, SERVICE_SCHEMA_SET_MODE, "set_mode" + SERVICE_SET_MODE, SERVICE_SCHEMA_SET_MODE, "async_set_mode" ) platform.async_register_entity_service( SERVICE_START_FLOW, SERVICE_SCHEMA_START_FLOW, _async_start_flow @@ -405,8 +390,6 @@ class YeelightGenericLight(YeelightEntity, LightEntity): self.config = device.config self._color_temp = None - self._hs = None - self._rgb = None self._effect = None model_specs = self._bulb.get_model_specs() @@ -420,19 +403,16 @@ class YeelightGenericLight(YeelightEntity, LightEntity): else: self._custom_effects = {} - @callback - def _schedule_immediate_update(self): - self.async_schedule_update_ha_state(True) - async def async_added_to_hass(self): """Handle entity which will be added.""" self.async_on_remove( async_dispatcher_connect( self.hass, DATA_UPDATED.format(self._device.host), - self._schedule_immediate_update, + self.async_write_ha_state, ) ) + await super().async_added_to_hass() @property def supported_features(self) -> int: @@ -502,16 +482,33 @@ class YeelightGenericLight(YeelightEntity, LightEntity): @property def hs_color(self) -> tuple: """Return the color property.""" - return self._hs + hue = self._get_property("hue") + sat = self._get_property("sat") + if hue is None or sat is None: + return None + + return (int(hue), int(sat)) @property def rgb_color(self) -> tuple: """Return the color property.""" - return self._rgb + rgb = self._get_property("rgb") + + if rgb is None: + return None + + rgb = int(rgb) + blue = rgb & 0xFF + green = (rgb >> 8) & 0xFF + red = (rgb >> 16) & 0xFF + + return (red, green, blue) @property def effect(self): """Return the current effect.""" + if not self.device.is_color_flow_enabled: + return None return self._effect @property @@ -561,33 +558,9 @@ class YeelightGenericLight(YeelightEntity, LightEntity): """Return yeelight device.""" return self._device - def update(self): + async def async_update(self): """Update light properties.""" - self._hs = self._get_hs_from_properties() - self._rgb = self._get_rgb_from_properties() - if not self.device.is_color_flow_enabled: - self._effect = None - - def _get_hs_from_properties(self): - hue = self._get_property("hue") - sat = self._get_property("sat") - if hue is None or sat is None: - return None - - return (int(hue), int(sat)) - - def _get_rgb_from_properties(self): - rgb = self._get_property("rgb") - - if rgb is None: - return None - - rgb = int(rgb) - blue = rgb & 0xFF - green = (rgb >> 8) & 0xFF - red = (rgb >> 16) & 0xFF - - return (red, green, blue) + await self.device.async_update() def set_music_mode(self, music_mode) -> None: """Set the music mode on or off.""" @@ -599,53 +572,51 @@ class YeelightGenericLight(YeelightEntity, LightEntity): else: self._bulb.stop_music() - self.device.update() - - @_cmd - def set_brightness(self, brightness, duration) -> None: + @_async_cmd + async def async_set_brightness(self, brightness, duration) -> None: """Set bulb brightness.""" if brightness: _LOGGER.debug("Setting brightness: %s", brightness) - self._bulb.set_brightness( + await self._bulb.async_set_brightness( brightness / 255 * 100, duration=duration, light_type=self.light_type ) - @_cmd - def set_hs(self, hs_color, duration) -> None: + @_async_cmd + async def async_set_hs(self, hs_color, duration) -> None: """Set bulb's color.""" if hs_color and COLOR_MODE_HS in self.supported_color_modes: _LOGGER.debug("Setting HS: %s", hs_color) - self._bulb.set_hsv( + await self._bulb.async_set_hsv( hs_color[0], hs_color[1], duration=duration, light_type=self.light_type ) - @_cmd - def set_rgb(self, rgb, duration) -> None: + @_async_cmd + async def async_set_rgb(self, rgb, duration) -> None: """Set bulb's color.""" if rgb and COLOR_MODE_RGB in self.supported_color_modes: _LOGGER.debug("Setting RGB: %s", rgb) - self._bulb.set_rgb( + await self._bulb.async_set_rgb( rgb[0], rgb[1], rgb[2], duration=duration, light_type=self.light_type ) - @_cmd - def set_colortemp(self, colortemp, duration) -> None: + @_async_cmd + async def async_set_colortemp(self, colortemp, duration) -> None: """Set bulb's color temperature.""" if colortemp and COLOR_MODE_COLOR_TEMP in self.supported_color_modes: temp_in_k = mired_to_kelvin(colortemp) _LOGGER.debug("Setting color temp: %s K", temp_in_k) - self._bulb.set_color_temp( + await self._bulb.async_set_color_temp( temp_in_k, duration=duration, light_type=self.light_type ) - @_cmd - def set_default(self) -> None: + @_async_cmd + async def async_set_default(self) -> None: """Set current options as default.""" - self._bulb.set_default() + await self._bulb.async_set_default() - @_cmd - def set_flash(self, flash) -> None: + @_async_cmd + async def async_set_flash(self, flash) -> None: """Activate flash.""" if flash: if int(self._bulb.last_properties["color_mode"]) != 1: @@ -660,7 +631,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): count = 1 duration = transition * 2 - red, green, blue = color_util.color_hs_to_RGB(*self._hs) + red, green, blue = color_util.color_hs_to_RGB(*self.hs_color) transitions = [] transitions.append( @@ -675,18 +646,18 @@ class YeelightGenericLight(YeelightEntity, LightEntity): flow = Flow(count=count, transitions=transitions) try: - self._bulb.start_flow(flow, light_type=self.light_type) + await self._bulb.async_start_flow(flow, light_type=self.light_type) except BulbException as ex: _LOGGER.error("Unable to set flash: %s", ex) - @_cmd - def set_effect(self, effect) -> None: + @_async_cmd + async def async_set_effect(self, effect) -> None: """Activate effect.""" if not effect: return if effect == EFFECT_STOP: - self._bulb.stop_flow(light_type=self.light_type) + await self._bulb.async_stop_flow(light_type=self.light_type) return if effect in self.custom_effects_names: @@ -705,12 +676,12 @@ class YeelightGenericLight(YeelightEntity, LightEntity): return try: - self._bulb.start_flow(flow, light_type=self.light_type) + await self._bulb.async_start_flow(flow, light_type=self.light_type) self._effect = effect except BulbException as ex: _LOGGER.error("Unable to set effect: %s", ex) - def turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs) -> None: """Turn the bulb on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) colortemp = kwargs.get(ATTR_COLOR_TEMP) @@ -723,15 +694,18 @@ class YeelightGenericLight(YeelightEntity, LightEntity): if ATTR_TRANSITION in kwargs: # passed kwarg overrides config duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s - self.device.turn_on( - duration=duration, - light_type=self.light_type, - power_mode=self._turn_on_power_mode, - ) + if not self.is_on: + await self.device.async_turn_on( + duration=duration, + light_type=self.light_type, + power_mode=self._turn_on_power_mode, + ) if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode: try: - self.set_music_mode(self.config[CONF_MODE_MUSIC]) + await self.hass.async_add_executor_job( + self.set_music_mode, self.config[CONF_MODE_MUSIC] + ) except BulbException as ex: _LOGGER.error( "Unable to turn on music mode, consider disabling it: %s", ex @@ -739,12 +713,12 @@ class YeelightGenericLight(YeelightEntity, LightEntity): try: # values checked for none in methods - self.set_hs(hs_color, duration) - self.set_rgb(rgb, duration) - self.set_colortemp(colortemp, duration) - self.set_brightness(brightness, duration) - self.set_flash(flash) - self.set_effect(effect) + await self.async_set_hs(hs_color, duration) + await self.async_set_rgb(rgb, duration) + await self.async_set_colortemp(colortemp, duration) + await self.async_set_brightness(brightness, duration) + await self.async_set_flash(flash) + await self.async_set_effect(effect) except BulbException as ex: _LOGGER.error("Unable to set bulb properties: %s", ex) return @@ -752,50 +726,48 @@ class YeelightGenericLight(YeelightEntity, LightEntity): # save the current state if we had a manual change. if self.config[CONF_SAVE_ON_CHANGE] and (brightness or colortemp or rgb): try: - self.set_default() + await self.async_set_default() except BulbException as ex: _LOGGER.error("Unable to set the defaults: %s", ex) return - self.device.update() - def turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: """Turn off.""" + if not self.is_on: + return + duration = int(self.config[CONF_TRANSITION]) # in ms if ATTR_TRANSITION in kwargs: # passed kwarg overrides config duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s - self.device.turn_off(duration=duration, light_type=self.light_type) - self.device.update() + await self.device.async_turn_off(duration=duration, light_type=self.light_type) - def set_mode(self, mode: str): + async def async_set_mode(self, mode: str): """Set a power mode.""" try: - self._bulb.set_power_mode(PowerMode[mode.upper()]) - self.device.update() + await self._bulb.async_set_power_mode(PowerMode[mode.upper()]) except BulbException as ex: _LOGGER.error("Unable to set the power mode: %s", ex) - def start_flow(self, transitions, count=0, action=ACTION_RECOVER): + async def async_start_flow(self, transitions, count=0, action=ACTION_RECOVER): """Start flow.""" try: flow = Flow( count=count, action=Flow.actions[action], transitions=transitions ) - self._bulb.start_flow(flow, light_type=self.light_type) - self.device.update() + await self._bulb.async_start_flow(flow, light_type=self.light_type) except BulbException as ex: _LOGGER.error("Unable to set effect: %s", ex) - def set_scene(self, scene_class, *args): + async def async_set_scene(self, scene_class, *args): """ Set the light directly to the specified state. If the light is off, it will first be turned on. """ try: - self._bulb.set_scene(scene_class, *args) - self.device.update() + await self._bulb.async_set_scene(scene_class, *args) except BulbException as ex: _LOGGER.error("Unable to set scene: %s", ex) diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 0bf6249b647..7b78f540289 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,10 +2,10 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.6.3"], - "codeowners": ["@rytilahti", "@zewelor", "@shenxn"], + "requirements": ["yeelight==0.7.2"], + "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"], "config_flow": true, - "iot_class": "local_polling", + "iot_class": "local_push", "dhcp": [{ "hostname": "yeelink-*" }], diff --git a/requirements_all.txt b/requirements_all.txt index 0f2e0fdf1c3..1ce754f9db1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2421,7 +2421,7 @@ yalesmartalarmclient==0.3.4 yalexs==1.1.13 # homeassistant.components.yeelight -yeelight==0.6.3 +yeelight==0.7.2 # homeassistant.components.yeelightsunflower yeelightsunflower==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d224c4112d..f5fc75e41ae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1338,7 +1338,7 @@ yalesmartalarmclient==0.3.4 yalexs==1.1.13 # homeassistant.components.yeelight -yeelight==0.6.3 +yeelight==0.7.2 # homeassistant.components.youless youless-api==0.10 diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index 5725880f942..9fa864d6213 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -1,5 +1,5 @@ """Tests for the Yeelight integration.""" -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from yeelight import BulbException, BulbType from yeelight.main import _MODEL_SPECS @@ -84,16 +84,34 @@ def _mocked_bulb(cannot_connect=False): type(bulb).get_capabilities = MagicMock( return_value=None if cannot_connect else CAPABILITIES ) + type(bulb).async_get_properties = AsyncMock( + side_effect=BulbException if cannot_connect else None + ) type(bulb).get_properties = MagicMock( side_effect=BulbException if cannot_connect else None ) type(bulb).get_model_specs = MagicMock(return_value=_MODEL_SPECS[MODEL]) - bulb.capabilities = CAPABILITIES + bulb.capabilities = CAPABILITIES.copy() bulb.model = MODEL bulb.bulb_type = BulbType.Color - bulb.last_properties = PROPERTIES + bulb.last_properties = PROPERTIES.copy() bulb.music_mode = False + bulb.async_get_properties = AsyncMock() + bulb.async_listen = AsyncMock() + bulb.async_stop_listening = AsyncMock() + bulb.async_update = AsyncMock() + bulb.async_turn_on = AsyncMock() + bulb.async_turn_off = AsyncMock() + bulb.async_set_brightness = AsyncMock() + bulb.async_set_color_temp = AsyncMock() + bulb.async_set_hsv = AsyncMock() + bulb.async_set_rgb = AsyncMock() + bulb.async_start_flow = AsyncMock() + bulb.async_stop_flow = AsyncMock() + bulb.async_set_power_mode = AsyncMock() + bulb.async_set_scene = AsyncMock() + bulb.async_set_default = AsyncMock() return bulb diff --git a/tests/components/yeelight/test_binary_sensor.py b/tests/components/yeelight/test_binary_sensor.py index f716469fc9a..472d8de4919 100644 --- a/tests/components/yeelight/test_binary_sensor.py +++ b/tests/components/yeelight/test_binary_sensor.py @@ -14,7 +14,7 @@ ENTITY_BINARY_SENSOR = f"binary_sensor.{NAME}_nightlight" async def test_nightlight(hass: HomeAssistant): """Test nightlight sensor.""" mocked_bulb = _mocked_bulb() - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch( + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), patch( f"{MODULE}.config_flow.yeelight.Bulb", return_value=mocked_bulb ): await async_setup_component(hass, DOMAIN, YAML_CONFIGURATION) diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 8994c8e3360..247630ecfc3 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -219,7 +219,7 @@ async def test_options(hass: HomeAssistant): config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -241,7 +241,7 @@ async def test_options(hass: HomeAssistant): config[CONF_NIGHTLIGHT_SWITCH] = True user_input = {**config} user_input.pop(CONF_NAME) - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input ) diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index 2d1113d1896..575ad4cb594 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -1,7 +1,7 @@ """Test Yeelight.""" -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch -from yeelight import BulbType +from yeelight import BulbException, BulbType from homeassistant.components.yeelight import ( CONF_NIGHTLIGHT_SWITCH, @@ -56,7 +56,7 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant): ) _discovered_devices = [{"capabilities": CAPABILITIES, "ip": IP_ADDRESS}] - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch( + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), patch( f"{MODULE}.discover_bulbs", return_value=_discovered_devices ): assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -65,14 +65,12 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant): binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format( f"yeelight_color_{ID}" ) - entity_registry = er.async_get(hass) - assert entity_registry.async_get(binary_sensor_entity_id) is None - await hass.async_block_till_done() + type(mocked_bulb).async_get_properties = AsyncMock(None) - type(mocked_bulb).get_properties = MagicMock(None) - - hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE].update() + await hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][ + DATA_DEVICE + ].async_update() await hass.async_block_till_done() await hass.async_block_till_done() @@ -91,7 +89,7 @@ async def test_ip_changes_id_missing_cannot_fallback(hass: HomeAssistant): side_effect=[OSError, CAPABILITIES, CAPABILITIES] ) - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -104,7 +102,9 @@ async def test_setup_discovery(hass: HomeAssistant): config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with _patch_discovery(MODULE), patch( + f"{MODULE}.AsyncBulb", return_value=mocked_bulb + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -127,7 +127,7 @@ async def test_setup_import(hass: HomeAssistant): """Test import from yaml.""" mocked_bulb = _mocked_bulb() name = "yeelight" - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch( + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), patch( f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb ): assert await async_setup_component( @@ -162,7 +162,9 @@ async def test_unique_ids_device(hass: HomeAssistant): mocked_bulb = _mocked_bulb() mocked_bulb.bulb_type = BulbType.WhiteTempMood - with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with _patch_discovery(MODULE), patch( + f"{MODULE}.AsyncBulb", return_value=mocked_bulb + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -186,7 +188,9 @@ async def test_unique_ids_entry(hass: HomeAssistant): mocked_bulb = _mocked_bulb() mocked_bulb.bulb_type = BulbType.WhiteTempMood - with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with _patch_discovery(MODULE), patch( + f"{MODULE}.AsyncBulb", return_value=mocked_bulb + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -216,7 +220,7 @@ async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant): mocked_bulb = _mocked_bulb(True) mocked_bulb.bulb_type = BulbType.WhiteTempMood - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch( + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), patch( f"{MODULE}.config_flow.yeelight.Bulb", return_value=mocked_bulb ): assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -225,15 +229,52 @@ async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant): binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format( IP_ADDRESS.replace(".", "_") ) - entity_registry = er.async_get(hass) - assert entity_registry.async_get(binary_sensor_entity_id) is None type(mocked_bulb).get_capabilities = MagicMock(CAPABILITIES) type(mocked_bulb).get_properties = MagicMock(None) - hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE].update() + await hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][ + DATA_DEVICE + ].async_update() + hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][ + DATA_DEVICE + ].async_update_callback({}) await hass.async_block_till_done() await hass.async_block_till_done() entity_registry = er.async_get(hass) assert entity_registry.async_get(binary_sensor_entity_id) is not None + + +async def test_async_listen_error_late_discovery(hass, caplog): + """Test the async listen error.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) + config_entry.add_to_hass(hass) + + mocked_bulb = _mocked_bulb() + mocked_bulb.async_listen = AsyncMock(side_effect=BulbException) + + with _patch_discovery(MODULE), 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 "Failed to connect to bulb at" in caplog.text + + +async def test_async_listen_error_has_host(hass: HomeAssistant): + """Test the async listen error.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_ID: ID, CONF_HOST: "127.0.0.1"} + ) + config_entry.add_to_hass(hass) + + mocked_bulb = _mocked_bulb() + mocked_bulb.async_listen = AsyncMock(side_effect=BulbException) + + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): + await hass.config_entries.async_setup(config_entry.entry_id) + + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index 9283514cb70..9a1f632242b 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -1,6 +1,6 @@ """Test the Yeelight light.""" import logging -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from yeelight import ( BulbException, @@ -131,7 +131,9 @@ async def test_services(hass: HomeAssistant, caplog): config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with _patch_discovery(MODULE), patch( + f"{MODULE}.AsyncBulb", return_value=mocked_bulb + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -146,8 +148,11 @@ async def test_services(hass: HomeAssistant, caplog): err_count = len([x for x in caplog.records if x.levelno == logging.ERROR]) # success - mocked_method = MagicMock() - setattr(type(mocked_bulb), method, mocked_method) + if method.startswith("async_"): + mocked_method = AsyncMock() + else: + mocked_method = MagicMock() + setattr(mocked_bulb, method, mocked_method) await hass.services.async_call(domain, service, data, blocking=True) if payload is None: mocked_method.assert_called_once() @@ -161,8 +166,11 @@ async def test_services(hass: HomeAssistant, caplog): # failure if failure_side_effect: - mocked_method = MagicMock(side_effect=failure_side_effect) - setattr(type(mocked_bulb), method, mocked_method) + if method.startswith("async_"): + mocked_method = AsyncMock(side_effect=failure_side_effect) + else: + mocked_method = MagicMock(side_effect=failure_side_effect) + setattr(mocked_bulb, method, mocked_method) await hass.services.async_call(domain, service, data, blocking=True) assert ( len([x for x in caplog.records if x.levelno == logging.ERROR]) @@ -173,6 +181,7 @@ async def test_services(hass: HomeAssistant, caplog): brightness = 100 rgb_color = (0, 128, 255) transition = 2 + mocked_bulb.last_properties["power"] = "off" await hass.services.async_call( "light", SERVICE_TURN_ON, @@ -186,30 +195,30 @@ async def test_services(hass: HomeAssistant, caplog): }, blocking=True, ) - mocked_bulb.turn_on.assert_called_once_with( + mocked_bulb.async_turn_on.assert_called_once_with( duration=transition * 1000, light_type=LightType.Main, power_mode=PowerMode.NORMAL, ) - mocked_bulb.turn_on.reset_mock() + mocked_bulb.async_turn_on.reset_mock() mocked_bulb.start_music.assert_called_once() mocked_bulb.start_music.reset_mock() - mocked_bulb.set_brightness.assert_called_once_with( + mocked_bulb.async_set_brightness.assert_called_once_with( brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main ) - mocked_bulb.set_brightness.reset_mock() - mocked_bulb.set_color_temp.assert_not_called() - mocked_bulb.set_color_temp.reset_mock() - mocked_bulb.set_hsv.assert_not_called() - mocked_bulb.set_hsv.reset_mock() - mocked_bulb.set_rgb.assert_called_once_with( + mocked_bulb.async_set_brightness.reset_mock() + mocked_bulb.async_set_color_temp.assert_not_called() + mocked_bulb.async_set_color_temp.reset_mock() + mocked_bulb.async_set_hsv.assert_not_called() + mocked_bulb.async_set_hsv.reset_mock() + mocked_bulb.async_set_rgb.assert_called_once_with( *rgb_color, duration=transition * 1000, light_type=LightType.Main ) - mocked_bulb.set_rgb.reset_mock() - mocked_bulb.start_flow.assert_called_once() # flash - mocked_bulb.start_flow.reset_mock() - mocked_bulb.stop_flow.assert_called_once_with(light_type=LightType.Main) - mocked_bulb.stop_flow.reset_mock() + mocked_bulb.async_set_rgb.reset_mock() + mocked_bulb.async_start_flow.assert_called_once() # flash + mocked_bulb.async_start_flow.reset_mock() + mocked_bulb.async_stop_flow.assert_called_once_with(light_type=LightType.Main) + mocked_bulb.async_stop_flow.reset_mock() # turn_on hs_color brightness = 100 @@ -228,35 +237,36 @@ async def test_services(hass: HomeAssistant, caplog): }, blocking=True, ) - mocked_bulb.turn_on.assert_called_once_with( + mocked_bulb.async_turn_on.assert_called_once_with( duration=transition * 1000, light_type=LightType.Main, power_mode=PowerMode.NORMAL, ) - mocked_bulb.turn_on.reset_mock() + mocked_bulb.async_turn_on.reset_mock() mocked_bulb.start_music.assert_called_once() mocked_bulb.start_music.reset_mock() - mocked_bulb.set_brightness.assert_called_once_with( + mocked_bulb.async_set_brightness.assert_called_once_with( brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main ) - mocked_bulb.set_brightness.reset_mock() - mocked_bulb.set_color_temp.assert_not_called() - mocked_bulb.set_color_temp.reset_mock() - mocked_bulb.set_hsv.assert_called_once_with( + mocked_bulb.async_set_brightness.reset_mock() + mocked_bulb.async_set_color_temp.assert_not_called() + mocked_bulb.async_set_color_temp.reset_mock() + mocked_bulb.async_set_hsv.assert_called_once_with( *hs_color, duration=transition * 1000, light_type=LightType.Main ) - mocked_bulb.set_hsv.reset_mock() - mocked_bulb.set_rgb.assert_not_called() - mocked_bulb.set_rgb.reset_mock() - mocked_bulb.start_flow.assert_called_once() # flash - mocked_bulb.start_flow.reset_mock() - mocked_bulb.stop_flow.assert_called_once_with(light_type=LightType.Main) - mocked_bulb.stop_flow.reset_mock() + mocked_bulb.async_set_hsv.reset_mock() + mocked_bulb.async_set_rgb.assert_not_called() + mocked_bulb.async_set_rgb.reset_mock() + mocked_bulb.async_start_flow.assert_called_once() # flash + mocked_bulb.async_start_flow.reset_mock() + mocked_bulb.async_stop_flow.assert_called_once_with(light_type=LightType.Main) + mocked_bulb.async_stop_flow.reset_mock() # turn_on color_temp brightness = 100 color_temp = 200 transition = 1 + mocked_bulb.last_properties["power"] = "off" await hass.services.async_call( "light", SERVICE_TURN_ON, @@ -270,31 +280,32 @@ async def test_services(hass: HomeAssistant, caplog): }, blocking=True, ) - mocked_bulb.turn_on.assert_called_once_with( + mocked_bulb.async_turn_on.assert_called_once_with( duration=transition * 1000, light_type=LightType.Main, power_mode=PowerMode.NORMAL, ) - mocked_bulb.turn_on.reset_mock() + mocked_bulb.async_turn_on.reset_mock() mocked_bulb.start_music.assert_called_once() - mocked_bulb.set_brightness.assert_called_once_with( + mocked_bulb.async_set_brightness.assert_called_once_with( brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main ) - mocked_bulb.set_color_temp.assert_called_once_with( + mocked_bulb.async_set_color_temp.assert_called_once_with( color_temperature_mired_to_kelvin(color_temp), duration=transition * 1000, light_type=LightType.Main, ) - mocked_bulb.set_hsv.assert_not_called() - mocked_bulb.set_rgb.assert_not_called() - mocked_bulb.start_flow.assert_called_once() # flash - mocked_bulb.stop_flow.assert_called_once_with(light_type=LightType.Main) + mocked_bulb.async_set_hsv.assert_not_called() + mocked_bulb.async_set_rgb.assert_not_called() + mocked_bulb.async_start_flow.assert_called_once() # flash + mocked_bulb.async_stop_flow.assert_called_once_with(light_type=LightType.Main) + mocked_bulb.last_properties["power"] = "off" # turn_on nightlight await _async_test_service( SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_NIGHTLIGHT}, - "turn_on", + "async_turn_on", payload={ "duration": DEFAULT_TRANSITION, "light_type": LightType.Main, @@ -303,11 +314,12 @@ async def test_services(hass: HomeAssistant, caplog): domain="light", ) + mocked_bulb.last_properties["power"] = "on" # turn_off await _async_test_service( SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_TRANSITION: transition}, - "turn_off", + "async_turn_off", domain="light", payload={"duration": transition * 1000, "light_type": LightType.Main}, ) @@ -317,7 +329,7 @@ async def test_services(hass: HomeAssistant, caplog): await _async_test_service( SERVICE_SET_MODE, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MODE: "rgb"}, - "set_power_mode", + "async_set_power_mode", [PowerMode[mode.upper()]], ) @@ -328,7 +340,7 @@ async def test_services(hass: HomeAssistant, caplog): ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_TRANSITIONS: [{YEELIGHT_TEMPERATURE_TRANSACTION: [1900, 2000, 60]}], }, - "start_flow", + "async_start_flow", ) # set_color_scene @@ -339,7 +351,7 @@ async def test_services(hass: HomeAssistant, caplog): ATTR_RGB_COLOR: [10, 20, 30], ATTR_BRIGHTNESS: 50, }, - "set_scene", + "async_set_scene", [SceneClass.COLOR, 10, 20, 30, 50], ) @@ -347,7 +359,7 @@ async def test_services(hass: HomeAssistant, caplog): await _async_test_service( SERVICE_SET_HSV_SCENE, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_HS_COLOR: [180, 50], ATTR_BRIGHTNESS: 50}, - "set_scene", + "async_set_scene", [SceneClass.HSV, 180, 50, 50], ) @@ -355,7 +367,7 @@ async def test_services(hass: HomeAssistant, caplog): await _async_test_service( SERVICE_SET_COLOR_TEMP_SCENE, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_KELVIN: 4000, ATTR_BRIGHTNESS: 50}, - "set_scene", + "async_set_scene", [SceneClass.CT, 4000, 50], ) @@ -366,14 +378,14 @@ async def test_services(hass: HomeAssistant, caplog): ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_TRANSITIONS: [{YEELIGHT_TEMPERATURE_TRANSACTION: [1900, 2000, 60]}], }, - "set_scene", + "async_set_scene", ) # set_auto_delay_off_scene await _async_test_service( SERVICE_SET_AUTO_DELAY_OFF_SCENE, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MINUTES: 1, ATTR_BRIGHTNESS: 50}, - "set_scene", + "async_set_scene", [SceneClass.AUTO_DELAY_OFF, 50, 1], ) @@ -401,6 +413,7 @@ async def test_services(hass: HomeAssistant, caplog): failure_side_effect=None, ) # test _cmd wrapper error handler + mocked_bulb.last_properties["power"] = "off" err_count = len([x for x in caplog.records if x.levelno == logging.ERROR]) type(mocked_bulb).turn_on = MagicMock() type(mocked_bulb).set_brightness = MagicMock(side_effect=BulbException) @@ -424,8 +437,11 @@ async def test_device_types(hass: HomeAssistant, caplog): mocked_bulb.last_properties = properties async def _async_setup(config_entry): - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): - await hass.config_entries.async_setup(config_entry.entry_id) + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + # We use asyncio.create_task now to avoid + # blocking starting so we need to block again await hass.async_block_till_done() async def _async_test( @@ -447,6 +463,7 @@ async def test_device_types(hass: HomeAssistant, caplog): await _async_setup(config_entry) state = hass.states.get(entity_id) + assert state.state == "on" target_properties["friendly_name"] = name target_properties["flowing"] = False @@ -481,6 +498,7 @@ async def test_device_types(hass: HomeAssistant, caplog): await hass.config_entries.async_unload(config_entry.entry_id) await config_entry.async_remove(hass) registry.async_clear_config_entry(config_entry.entry_id) + await hass.async_block_till_done() bright = round(255 * int(PROPERTIES["bright"]) / 100) current_brightness = round(255 * int(PROPERTIES["current_brightness"]) / 100) @@ -841,7 +859,9 @@ async def test_effects(hass: HomeAssistant): config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with _patch_discovery(MODULE), patch( + f"{MODULE}.AsyncBulb", return_value=mocked_bulb + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -850,8 +870,8 @@ async def test_effects(hass: HomeAssistant): ) == YEELIGHT_COLOR_EFFECT_LIST + ["mock_effect"] async def _async_test_effect(name, target=None, called=True): - mocked_start_flow = MagicMock() - type(mocked_bulb).start_flow = mocked_start_flow + async_mocked_start_flow = AsyncMock() + mocked_bulb.async_start_flow = async_mocked_start_flow await hass.services.async_call( "light", SERVICE_TURN_ON, @@ -860,10 +880,10 @@ async def test_effects(hass: HomeAssistant): ) if not called: return - mocked_start_flow.assert_called_once() + async_mocked_start_flow.assert_called_once() if target is None: return - args, _ = mocked_start_flow.call_args + args, _ = async_mocked_start_flow.call_args flow = args[0] assert flow.count == target.count assert flow.action == target.action From 74d41ac5e52f05c36843b6b438be53e8676ddf3a Mon Sep 17 00:00:00 2001 From: Reuben Gow Date: Mon, 9 Aug 2021 19:47:38 +0100 Subject: [PATCH 073/355] Force an attempted subscribe on speaker reboot (#54100) * Force an attempted subscribe on speaker reboot * Recreate subscriptions and timers explicitly on speaker reboot * only create poll timer if there is not one already Co-authored-by: jjlawren * Black Co-authored-by: jjlawren --- homeassistant/components/sonos/speaker.py | 29 ++++++++++++++++------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 434717f7a85..919e03cf39b 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -496,9 +496,7 @@ class SonosSpeaker: self.async_write_entity_states() - async def async_unseen( - self, now: datetime.datetime | None = None, will_reconnect: bool = False - ) -> None: + async def async_unseen(self, now: datetime.datetime | None = None) -> None: """Make this player unavailable when it was not seen recently.""" if self._seen_timer: self._seen_timer() @@ -527,9 +525,8 @@ class SonosSpeaker: await self.async_unsubscribe() - if not will_reconnect: - self.hass.data[DATA_SONOS].discovery_known.discard(self.soco.uid) - self.async_write_entity_states() + self.hass.data[DATA_SONOS].discovery_known.discard(self.soco.uid) + self.async_write_entity_states() async def async_rebooted(self, soco: SoCo) -> None: """Handle a detected speaker reboot.""" @@ -538,8 +535,24 @@ class SonosSpeaker: self.zone_name, soco, ) - await self.async_unseen(will_reconnect=True) - await self.async_seen(soco) + await self.async_unsubscribe() + self.soco = soco + await self.async_subscribe() + if self._seen_timer: + self._seen_timer() + self._seen_timer = self.hass.helpers.event.async_call_later( + SEEN_EXPIRE_TIME.total_seconds(), self.async_unseen + ) + if not self._poll_timer: + self._poll_timer = self.hass.helpers.event.async_track_time_interval( + partial( + async_dispatcher_send, + self.hass, + f"{SONOS_POLL_UPDATE}-{self.soco.uid}", + ), + SCAN_INTERVAL, + ) + self.async_write_entity_states() # # Battery management From 511af66b22186d795a0dfbbfb05100b806cee345 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 9 Aug 2021 20:53:30 +0200 Subject: [PATCH 074/355] Fix ondilo_ico name attribute (#54290) --- homeassistant/components/ondilo_ico/sensor.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index 26a61ddfe4c..7449524d9e5 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -141,9 +141,9 @@ class OndiloICO(CoordinatorEntity, SensorEntity): self._poolid = self.coordinator.data[poolidx]["id"] pooldata = self._pooldata() - self._unique_id = f"{pooldata['ICO']['serial_number']}-{description.key}" + self._attr_unique_id = f"{pooldata['ICO']['serial_number']}-{description.key}" self._device_name = pooldata["name"] - self._name = f"{self._device_name} {description.name}" + self._attr_name = f"{self._device_name} {description.name}" def _pooldata(self): """Get pool data dict.""" @@ -168,11 +168,6 @@ class OndiloICO(CoordinatorEntity, SensorEntity): """Last value of the sensor.""" return self._devdata()["value"] - @property - def unique_id(self): - """Return the unique ID of this entity.""" - return self._unique_id - @property def device_info(self): """Return the device info for the sensor.""" From a54ee7b3664079399111fdd4a72c69c6ed81201e Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 9 Aug 2021 14:55:58 -0400 Subject: [PATCH 075/355] Use correct state attribute for alarmdecoder binary sensor (#54286) Co-authored-by: Martin Hjelmare --- homeassistant/components/alarmdecoder/binary_sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index 397394e256b..430a4f73262 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -111,13 +111,13 @@ class AlarmDecoderBinarySensor(BinarySensorEntity): def _fault_callback(self, zone): """Update the zone's state, if needed.""" if zone is None or int(zone) == self._zone_number: - self._attr_state = 1 + self._attr_is_on = True self.schedule_update_ha_state() def _restore_callback(self, zone): """Update the zone's state, if needed.""" if zone is None or (int(zone) == self._zone_number and not self._loop): - self._attr_state = 0 + self._attr_is_on = False self.schedule_update_ha_state() def _rfx_message_callback(self, message): @@ -125,7 +125,7 @@ class AlarmDecoderBinarySensor(BinarySensorEntity): if self._rfid and message and message.serial_number == self._rfid: rfstate = message.value if self._loop: - self._attr_state = 1 if message.loop[self._loop - 1] else 0 + self._attr_is_on = bool(message.loop[self._loop - 1]) attr = {CONF_ZONE_NUMBER: self._zone_number} if self._rfid and rfstate is not None: attr[ATTR_RF_BIT0] = bool(rfstate & 0x01) @@ -150,5 +150,5 @@ class AlarmDecoderBinarySensor(BinarySensorEntity): message.channel, message.value, ) - self._attr_state = message.value + self._attr_is_on = bool(message.value) self.schedule_update_ha_state() From 01490958246f319bb6b1264d3312e12e505b63a9 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 9 Aug 2021 20:57:36 +0200 Subject: [PATCH 076/355] Fix xiaomi air fresh fan preset modes (#54342) --- homeassistant/components/xiaomi_miio/fan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index c58d9ad0c66..05e32507b20 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -1138,6 +1138,7 @@ class XiaomiAirFresh(XiaomiGenericDevice): self._speed_list = OPERATION_MODES_AIRFRESH self._speed_count = 4 self._preset_modes = PRESET_MODES_AIRFRESH + self._supported_features = SUPPORT_SET_SPEED | SUPPORT_PRESET_MODE self._state_attrs.update( {attribute: None for attribute in self._available_attributes} ) From f37b164d602bf79ff446d425e513aa5e634fae80 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Aug 2021 13:58:27 -0500 Subject: [PATCH 077/355] Bump zeroconf to 0.34.3 (#54294) --- 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 1847a1c806b..83db312601c 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.33.4"], + "requirements": ["zeroconf==0.34.3"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6d22aa51b24..963cccf9ad2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ sqlalchemy==1.4.17 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 -zeroconf==0.33.4 +zeroconf==0.34.3 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 1ce754f9db1..c6d29e84dec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2439,7 +2439,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.33.4 +zeroconf==0.34.3 # homeassistant.components.zha zha-quirks==0.0.59 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f5fc75e41ae..a96149ad0b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1347,7 +1347,7 @@ youless-api==0.10 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.33.4 +zeroconf==0.34.3 # homeassistant.components.zha zha-quirks==0.0.59 From 8eff0e9312b734b8965befb9d2c04c0f2b9f7498 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Aug 2021 14:03:55 -0500 Subject: [PATCH 078/355] Ensure hunterdouglas_powerview model type is a string (#54299) --- homeassistant/components/hunterdouglas_powerview/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hunterdouglas_powerview/entity.py b/homeassistant/components/hunterdouglas_powerview/entity.py index bf0d5d564ff..db4b984703c 100644 --- a/homeassistant/components/hunterdouglas_powerview/entity.py +++ b/homeassistant/components/hunterdouglas_powerview/entity.py @@ -71,7 +71,7 @@ class ShadeEntity(HDEntity): "name": self._shade_name, "suggested_area": self._room_name, "manufacturer": MANUFACTURER, - "model": self._shade.raw_data[ATTR_TYPE], + "model": str(self._shade.raw_data[ATTR_TYPE]), "via_device": (DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER]), } From 7050b532648a6444394309d813bbec9a457faddd Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Mon, 9 Aug 2021 21:11:53 +0200 Subject: [PATCH 079/355] Fix login to BMW services for rest_of_world and north_america (#54261) 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 17aaa166942..8131ac1415c 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.7.16"], + "requirements": ["bimmer_connected==0.7.18"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index c6d29e84dec..1d52b811eb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -368,7 +368,7 @@ beautifulsoup4==4.9.3 bellows==0.26.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.16 +bimmer_connected==0.7.18 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a96149ad0b4..1bec0b54f5b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -223,7 +223,7 @@ base36==0.1.1 bellows==0.26.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.16 +bimmer_connected==0.7.18 # homeassistant.components.blebox blebox_uniapi==1.3.3 From 74a30af79ba491197d3f48c7b07e2e2217249de7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Aug 2021 14:13:55 -0500 Subject: [PATCH 080/355] Always set interfaces explicitly when IPv6 is present (#54268) --- homeassistant/components/zeroconf/__init__.py | 17 +++---- tests/components/zeroconf/test_init.py | 47 +++++++++++++++++-- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index cdb46318578..e7132f56b55 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -142,7 +142,15 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: zc_args: dict = {} adapters = await network.async_get_adapters(hass) - if _async_use_default_interface(adapters): + + ipv6 = True + if not any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters): + ipv6 = False + zc_args["ip_version"] = IPVersion.V4Only + else: + zc_args["ip_version"] = IPVersion.All + + if not ipv6 and _async_use_default_interface(adapters): zc_args["interfaces"] = InterfaceChoice.Default else: interfaces = zc_args["interfaces"] = [] @@ -158,13 +166,6 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: if adapter["ipv6"] and adapter["index"] not in interfaces: interfaces.append(adapter["index"]) - ipv6 = True - if not any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters): - ipv6 = False - zc_args["ip_version"] = IPVersion.V4Only - else: - zc_args["ip_version"] = IPVersion.All - aio_zc = await _async_get_instance(hass, **zc_args) zeroconf = cast(HaZeroconf, aio_zc.zeroconf) zeroconf_types, homekit_models = await asyncio.gather( diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index e1e346621fe..0db8f0f5227 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -794,11 +794,6 @@ async def test_async_detect_interfaces_setting_empty_route(hass, mock_async_zero ), patch( "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_service_info_mock, - ), patch( - "socket.if_nametoindex", - side_effect=lambda iface: {"eth0": 1, "eth1": 2, "eth2": 3, "vtun0": 4}.get( - iface, 0 - ), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -827,3 +822,45 @@ async def test_get_announced_addresses(hass, mock_async_zeroconf): first_ip = ip_address("192.168.1.5").packed actual = _get_announced_addresses(_ADAPTERS_WITH_MANUAL_CONFIG, first_ip) assert actual[0] == first_ip and set(actual) == expected + + +_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6 = [ + { + "auto": True, + "default": True, + "enabled": True, + "index": 1, + "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], + "ipv6": [ + { + "address": "fe80::dead:beef:dead:beef", + "network_prefix": 64, + "flowinfo": 1, + "scope_id": 3, + } + ], + "name": "eth1", + } +] + + +async def test_async_detect_interfaces_explicitly_set_ipv6(hass, mock_async_zeroconf): + """Test interfaces are explicitly set when IPv6 is present.""" + with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, patch.object( + hass.config_entries.flow, "async_init" + ), patch.object( + zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.network.async_get_adapters", + return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6, + ), patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_service_info_mock, + ): + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert mock_zc.mock_calls[0] == call( + interfaces=["192.168.1.5", 1], ip_version=IPVersion.All + ) From 38cb0553f3a791f9960af3fcb8fdeb683fc2dd87 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 9 Aug 2021 22:27:09 +0200 Subject: [PATCH 081/355] Update frontend to 20210809.0 (#54350) --- 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 b9a84cbec02..135c0ec0244 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==20210804.0"], + "requirements": [ + "home-assistant-frontend==20210809.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 963cccf9ad2..2e07d0adc18 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210804.0 +home-assistant-frontend==20210809.0 httpx==0.18.2 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 1d52b811eb9..b71b7fcc2c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -786,7 +786,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210804.0 +home-assistant-frontend==20210809.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1bec0b54f5b..774721010f8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -452,7 +452,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210804.0 +home-assistant-frontend==20210809.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From d79fc2c5066cd6235d2d21936ad1751b68d8a292 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 9 Aug 2021 23:38:58 +0200 Subject: [PATCH 082/355] Use EntityDescription - pi_hole (#54319) --- homeassistant/components/pi_hole/const.py | 83 +++++++++++++++------- homeassistant/components/pi_hole/sensor.py | 44 +++--------- 2 files changed, 69 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index f1871bf27c8..40a3a16de3a 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -1,6 +1,9 @@ """Constants for the pi_hole integration.""" +from __future__ import annotations + from datetime import timedelta +from homeassistant.components.sensor import SensorEntityDescription from homeassistant.const import PERCENTAGE DOMAIN = "pi_hole" @@ -25,28 +28,60 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) DATA_KEY_API = "api" DATA_KEY_COORDINATOR = "coordinator" -SENSOR_DICT = { - "ads_blocked_today": ["Ads Blocked Today", "ads", "mdi:close-octagon-outline"], - "ads_percentage_today": [ - "Ads Percentage Blocked Today", - PERCENTAGE, - "mdi:close-octagon-outline", - ], - "clients_ever_seen": ["Seen Clients", "clients", "mdi:account-outline"], - "dns_queries_today": [ - "DNS Queries Today", - "queries", - "mdi:comment-question-outline", - ], - "domains_being_blocked": ["Domains Blocked", "domains", "mdi:block-helper"], - "queries_cached": ["DNS Queries Cached", "queries", "mdi:comment-question-outline"], - "queries_forwarded": [ - "DNS Queries Forwarded", - "queries", - "mdi:comment-question-outline", - ], - "unique_clients": ["DNS Unique Clients", "clients", "mdi:account-outline"], - "unique_domains": ["DNS Unique Domains", "domains", "mdi:domain"], -} -SENSOR_LIST = list(SENSOR_DICT) +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="ads_blocked_today", + name="Ads Blocked Today", + unit_of_measurement="ads", + icon="mdi:close-octagon-outline", + ), + SensorEntityDescription( + key="ads_percentage_today", + name="Ads Percentage Blocked Today", + unit_of_measurement=PERCENTAGE, + icon="mdi:close-octagon-outline", + ), + SensorEntityDescription( + key="clients_ever_seen", + name="Seen Clients", + unit_of_measurement="clients", + icon="mdi:account-outline", + ), + SensorEntityDescription( + key="dns_queries_today", + name="DNS Queries Today", + unit_of_measurement="queries", + icon="mdi:comment-question-outline", + ), + SensorEntityDescription( + key="domains_being_blocked", + name="Domains Blocked", + unit_of_measurement="domains", + icon="mdi:block-helper", + ), + SensorEntityDescription( + key="queries_cached", + name="DNS Queries Cached", + unit_of_measurement="queries", + icon="mdi:comment-question-outline", + ), + SensorEntityDescription( + key="queries_forwarded", + name="DNS Queries Forwarded", + unit_of_measurement="queries", + icon="mdi:comment-question-outline", + ), + SensorEntityDescription( + key="unique_clients", + name="DNS Unique Clients", + unit_of_measurement="clients", + icon="mdi:account-outline", + ), + SensorEntityDescription( + key="unique_domains", + name="DNS Unique Domains", + unit_of_measurement="domains", + icon="mdi:domain", + ), +) diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index 95aee56f7cc..38b0b192e14 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -5,7 +5,7 @@ from typing import Any from hole import Hole -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant @@ -18,8 +18,7 @@ from .const import ( DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN as PIHOLE_DOMAIN, - SENSOR_DICT, - SENSOR_LIST, + SENSOR_TYPES, ) @@ -34,10 +33,10 @@ async def async_setup_entry( hole_data[DATA_KEY_API], hole_data[DATA_KEY_COORDINATOR], name, - sensor_name, entry.entry_id, + description, ) - for sensor_name in SENSOR_LIST + for description in SENSOR_TYPES ] async_add_entities(sensors, True) @@ -50,46 +49,23 @@ class PiHoleSensor(PiHoleEntity, SensorEntity): api: Hole, coordinator: DataUpdateCoordinator, name: str, - sensor_name: str, server_unique_id: str, + description: SensorEntityDescription, ) -> None: """Initialize a Pi-hole sensor.""" super().__init__(api, coordinator, name, server_unique_id) + self.entity_description = description - self._condition = sensor_name - - variable_info = SENSOR_DICT[sensor_name] - self._condition_name = variable_info[0] - self._unit_of_measurement = variable_info[1] - self._icon = variable_info[2] - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"{self._name} {self._condition_name}" - - @property - def unique_id(self) -> str: - """Return the unique id of the sensor.""" - return f"{self._server_unique_id}/{self._condition_name}" - - @property - def icon(self) -> str: - """Icon to use in the frontend, if any.""" - return self._icon - - @property - def unit_of_measurement(self) -> str: - """Return the unit the value is expressed in.""" - return self._unit_of_measurement + self._attr_name = f"{name} {description.name}" + self._attr_unique_id = f"{self._server_unique_id}/{description.name}" @property def state(self) -> Any: """Return the state of the device.""" try: - return round(self.api.data[self._condition], 2) + return round(self.api.data[self.entity_description.key], 2) except TypeError: - return self.api.data[self._condition] + return self.api.data[self.entity_description.key] @property def extra_state_attributes(self) -> dict[str, Any]: From 4133cc05ebe539a6574fc344e729b51dd24a5f10 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 9 Aug 2021 23:40:57 +0200 Subject: [PATCH 083/355] Use EntityDescription - abode (#54321) --- homeassistant/components/abode/sensor.py | 60 +++++++++++++++--------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index f1f744a5511..a0681e0440f 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -1,7 +1,9 @@ """Support for Abode Security System sensors.""" +from __future__ import annotations + import abodepy.helpers.constants as CONST -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, @@ -11,12 +13,23 @@ from homeassistant.const import ( from . import AbodeDevice from .const import DOMAIN -# Sensor types: Name, icon -SENSOR_TYPES = { - CONST.TEMP_STATUS_KEY: ["Temperature", DEVICE_CLASS_TEMPERATURE], - CONST.HUMI_STATUS_KEY: ["Humidity", DEVICE_CLASS_HUMIDITY], - CONST.LUX_STATUS_KEY: ["Lux", DEVICE_CLASS_ILLUMINANCE], -} +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key=CONST.TEMP_STATUS_KEY, + name="Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + ), + SensorEntityDescription( + key=CONST.HUMI_STATUS_KEY, + name="Humidity", + device_class=DEVICE_CLASS_HUMIDITY, + ), + SensorEntityDescription( + key=CONST.LUX_STATUS_KEY, + name="Lux", + device_class=DEVICE_CLASS_ILLUMINANCE, + ), +) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -26,10 +39,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_SENSOR): - for sensor_type in SENSOR_TYPES: - if sensor_type not in device.get_value(CONST.STATUSES_KEY): - continue - entities.append(AbodeSensor(data, device, sensor_type)) + conditions = device.get_value(CONST.STATUSES_KEY) + entities.extend( + [ + AbodeSensor(data, device, description) + for description in SENSOR_TYPES + if description.key in conditions + ] + ) async_add_entities(entities) @@ -37,26 +54,25 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AbodeSensor(AbodeDevice, SensorEntity): """A sensor implementation for Abode devices.""" - def __init__(self, data, device, sensor_type): + def __init__(self, data, device, description: SensorEntityDescription): """Initialize a sensor for an Abode device.""" super().__init__(data, device) - self._sensor_type = sensor_type - self._attr_name = f"{device.name} {SENSOR_TYPES[sensor_type][0]}" - self._attr_device_class = SENSOR_TYPES[self._sensor_type][1] - self._attr_unique_id = f"{device.device_uuid}-{sensor_type}" - if self._sensor_type == CONST.TEMP_STATUS_KEY: + self.entity_description = description + self._attr_name = f"{device.name} {description.name}" + self._attr_unique_id = f"{device.device_uuid}-{description.key}" + if description.key == CONST.TEMP_STATUS_KEY: self._attr_unit_of_measurement = device.temp_unit - elif self._sensor_type == CONST.HUMI_STATUS_KEY: + elif description.key == CONST.HUMI_STATUS_KEY: self._attr_unit_of_measurement = device.humidity_unit - elif self._sensor_type == CONST.LUX_STATUS_KEY: + elif description.key == CONST.LUX_STATUS_KEY: self._attr_unit_of_measurement = device.lux_unit @property def state(self): """Return the state of the sensor.""" - if self._sensor_type == CONST.TEMP_STATUS_KEY: + if self.entity_description.key == CONST.TEMP_STATUS_KEY: return self._device.temp - if self._sensor_type == CONST.HUMI_STATUS_KEY: + if self.entity_description.key == CONST.HUMI_STATUS_KEY: return self._device.humidity - if self._sensor_type == CONST.LUX_STATUS_KEY: + if self.entity_description.key == CONST.LUX_STATUS_KEY: return self._device.lux From d55c7640485806439307f93c26e7db5af312c1fa Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 9 Aug 2021 23:43:59 +0200 Subject: [PATCH 084/355] Fix Xiaomi-miio turn fan on with speed, percentage or preset (#54353) --- homeassistant/components/xiaomi_miio/fan.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 05e32507b20..feeadf2bccc 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -615,6 +615,10 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity): **kwargs, ) -> None: """Turn the device on.""" + result = await self._try_command( + "Turning the miio device on failed.", self._device.on + ) + # Remove the async_set_speed call is async_set_percentage and async_set_preset_modes have been implemented if speed: await self.async_set_speed(speed) @@ -623,10 +627,6 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity): await self.async_set_percentage(percentage) if preset_mode: await self.async_set_preset_mode(preset_mode) - else: - result = await self._try_command( - "Turning the miio device on failed.", self._device.on - ) if result: self._state = True From c07b1423ee6a185899020ae9d239e0fe1f379307 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Mon, 9 Aug 2021 23:45:15 +0200 Subject: [PATCH 085/355] Remove useless attribute in devolo Home Control (#54284) --- homeassistant/components/devolo_home_control/devolo_device.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/devolo_home_control/devolo_device.py b/homeassistant/components/devolo_home_control/devolo_device.py index 781799cbf37..03f850579be 100644 --- a/homeassistant/components/devolo_home_control/devolo_device.py +++ b/homeassistant/components/devolo_home_control/devolo_device.py @@ -45,7 +45,6 @@ class DevoloDeviceEntity(Entity): self.subscriber: Subscriber | None = None self.sync_callback = self._sync self._value: int - self._unit = "" async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" From dcf4eb5e0dc5b79905a1a8acaa12dcfa6075f53d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Aug 2021 16:49:51 -0500 Subject: [PATCH 086/355] Remove HomeKit event guards (#54343) --- .../components/homekit/type_covers.py | 28 ++++------- homeassistant/components/homekit/type_fans.py | 14 ++---- .../components/homekit/type_humidifiers.py | 11 ++--- .../components/homekit/type_lights.py | 26 ++++------- .../components/homekit/type_locks.py | 8 +--- .../components/homekit/type_media_players.py | 18 +++----- .../components/homekit/type_remotes.py | 9 ++-- .../homekit/type_security_systems.py | 19 ++++---- .../components/homekit/type_sensors.py | 39 +++++++--------- .../components/homekit/type_switches.py | 28 +++++------ .../components/homekit/type_thermostats.py | 46 ++++++------------- 11 files changed, 84 insertions(+), 162 deletions(-) diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 099eced62d3..4c501208ca5 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -178,18 +178,11 @@ class GarageDoorOpener(HomeAccessory): obstruction_detected = ( new_state.attributes[ATTR_OBSTRUCTION_DETECTED] is True ) - if self.char_obstruction_detected.value != obstruction_detected: - self.char_obstruction_detected.set_value(obstruction_detected) + self.char_obstruction_detected.set_value(obstruction_detected) - if ( - target_door_state is not None - and self.char_target_state.value != target_door_state - ): + if target_door_state is not None: self.char_target_state.set_value(target_door_state) - if ( - current_door_state is not None - and self.char_current_state.value != current_door_state - ): + if current_door_state is not None: self.char_current_state.set_value(current_door_state) @@ -260,10 +253,8 @@ class OpeningDeviceBase(HomeAccessory): # We'll have to normalize to [0,100] current_tilt = (current_tilt / 100.0 * 180.0) - 90.0 current_tilt = int(current_tilt) - if self.char_current_tilt.value != current_tilt: - self.char_current_tilt.set_value(current_tilt) - if self.char_target_tilt.value != current_tilt: - self.char_target_tilt.set_value(current_tilt) + self.char_current_tilt.set_value(current_tilt) + self.char_target_tilt.set_value(current_tilt) class OpeningDevice(OpeningDeviceBase, HomeAccessory): @@ -312,14 +303,11 @@ class OpeningDevice(OpeningDeviceBase, HomeAccessory): current_position = new_state.attributes.get(ATTR_CURRENT_POSITION) if isinstance(current_position, (float, int)): current_position = int(current_position) - if self.char_current_position.value != current_position: - self.char_current_position.set_value(current_position) - if self.char_target_position.value != current_position: - self.char_target_position.set_value(current_position) + self.char_current_position.set_value(current_position) + self.char_target_position.set_value(current_position) position_state = _hass_state_to_position_start(new_state.state) - if self.char_position_state.value != position_state: - self.char_position_state.set_value(position_state) + self.char_position_state.set_value(position_state) super().async_update_state(new_state) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index 1a0bb41774c..85157dd9367 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -193,16 +193,14 @@ class Fan(HomeAccessory): state = new_state.state if state in (STATE_ON, STATE_OFF): self._state = 1 if state == STATE_ON else 0 - if self.char_active.value != self._state: - self.char_active.set_value(self._state) + self.char_active.set_value(self._state) # Handle Direction if self.char_direction is not None: direction = new_state.attributes.get(ATTR_DIRECTION) if direction in (DIRECTION_FORWARD, DIRECTION_REVERSE): hk_direction = 1 if direction == DIRECTION_REVERSE else 0 - if self.char_direction.value != hk_direction: - self.char_direction.set_value(hk_direction) + self.char_direction.set_value(hk_direction) # Handle Speed if self.char_speed is not None and state != STATE_OFF: @@ -222,7 +220,7 @@ class Fan(HomeAccessory): # in order to avoid this incorrect behavior. if percentage == 0 and state == STATE_ON: percentage = 1 - if percentage is not None and self.char_speed.value != percentage: + if percentage is not None: self.char_speed.set_value(percentage) # Handle Oscillating @@ -230,11 +228,9 @@ class Fan(HomeAccessory): oscillating = new_state.attributes.get(ATTR_OSCILLATING) if isinstance(oscillating, bool): hk_oscillating = 1 if oscillating else 0 - if self.char_swing.value != hk_oscillating: - self.char_swing.set_value(hk_oscillating) + self.char_swing.set_value(hk_oscillating) current_preset_mode = new_state.attributes.get(ATTR_PRESET_MODE) for preset_mode, char in self.preset_mode_chars.items(): hk_value = 1 if preset_mode == current_preset_mode else 0 - if char.value != hk_value: - char.set_value(hk_value) + char.set_value(hk_value) diff --git a/homeassistant/components/homekit/type_humidifiers.py b/homeassistant/components/homekit/type_humidifiers.py index a4a73abf998..6371f883b09 100644 --- a/homeassistant/components/homekit/type_humidifiers.py +++ b/homeassistant/components/homekit/type_humidifiers.py @@ -224,8 +224,7 @@ class HumidifierDehumidifier(HomeAccessory): is_active = new_state.state == STATE_ON # Update active state - if self.char_active.value != is_active: - self.char_active.set_value(is_active) + self.char_active.set_value(is_active) # Set current state if is_active: @@ -235,13 +234,9 @@ class HumidifierDehumidifier(HomeAccessory): current_state = HC_STATE_DEHUMIDIFYING else: current_state = HC_STATE_INACTIVE - if self.char_current_humidifier_dehumidifier.value != current_state: - self.char_current_humidifier_dehumidifier.set_value(current_state) + self.char_current_humidifier_dehumidifier.set_value(current_state) # Update target humidity target_humidity = new_state.attributes.get(ATTR_HUMIDITY) - if ( - isinstance(target_humidity, (int, float)) - and self.char_target_humidity.value != target_humidity - ): + if isinstance(target_humidity, (int, float)): self.char_target_humidity.set_value(target_humidity) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 88e21272a4f..169130a194a 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -205,13 +205,10 @@ class Light(HomeAccessory): color_temp_mode = color_mode == COLOR_MODE_COLOR_TEMP primary_on_value = char_on_value if not color_temp_mode else 0 secondary_on_value = char_on_value if color_temp_mode else 0 - if self.char_on_primary.value != primary_on_value: - self.char_on_primary.set_value(primary_on_value) - if self.char_on_secondary.value != secondary_on_value: - self.char_on_secondary.set_value(secondary_on_value) + self.char_on_primary.set_value(primary_on_value) + self.char_on_secondary.set_value(secondary_on_value) else: - if self.char_on_primary.value != char_on_value: - self.char_on_primary.set_value(char_on_value) + self.char_on_primary.set_value(char_on_value) # Handle Brightness if self.is_brightness_supported: @@ -230,12 +227,8 @@ class Light(HomeAccessory): # order to avoid this incorrect behavior. if brightness == 0 and state == STATE_ON: brightness = 1 - if self.char_brightness_primary.value != brightness: - self.char_brightness_primary.set_value(brightness) - if ( - self.color_and_temp_supported - and self.char_brightness_secondary.value != brightness - ): + self.char_brightness_primary.set_value(brightness) + if self.color_and_temp_supported: self.char_brightness_secondary.set_value(brightness) # Handle color temperature @@ -243,8 +236,7 @@ class Light(HomeAccessory): color_temperature = attributes.get(ATTR_COLOR_TEMP) if isinstance(color_temperature, (int, float)): color_temperature = round(color_temperature, 0) - if self.char_color_temperature.value != color_temperature: - self.char_color_temperature.set_value(color_temperature) + self.char_color_temperature.set_value(color_temperature) # Handle Color if self.is_color_supported: @@ -252,7 +244,5 @@ class Light(HomeAccessory): if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)): hue = round(hue, 0) saturation = round(saturation, 0) - if hue != self.char_hue.value: - self.char_hue.set_value(hue) - if saturation != self.char_saturation.value: - self.char_saturation.set_value(saturation) + self.char_hue.set_value(hue) + self.char_saturation.set_value(saturation) diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index 3a10a0a2f5a..af7501e1869 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -106,14 +106,10 @@ class Lock(HomeAccessory): # LockTargetState only supports locked and unlocked # Must set lock target state before current state # or there will be no notification - if ( - target_lock_state is not None - and self.char_target_state.value != target_lock_state - ): + if target_lock_state is not None: self.char_target_state.set_value(target_lock_state) # Set lock current state ONLY after ensuring that # target state is correct or there will be no # notification - if self.char_current_state.value != current_lock_state: - self.char_current_state.set_value(current_lock_state) + self.char_current_state.set_value(current_lock_state) diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index 081053d2591..7be1b98dcdb 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -180,8 +180,7 @@ class MediaPlayer(HomeAccessory): _LOGGER.debug( '%s: Set current state for "on_off" to %s', self.entity_id, hk_state ) - if self.chars[FEATURE_ON_OFF].value != hk_state: - self.chars[FEATURE_ON_OFF].set_value(hk_state) + self.chars[FEATURE_ON_OFF].set_value(hk_state) if self.chars[FEATURE_PLAY_PAUSE]: hk_state = current_state == STATE_PLAYING @@ -190,8 +189,7 @@ class MediaPlayer(HomeAccessory): self.entity_id, hk_state, ) - if self.chars[FEATURE_PLAY_PAUSE].value != hk_state: - self.chars[FEATURE_PLAY_PAUSE].set_value(hk_state) + self.chars[FEATURE_PLAY_PAUSE].set_value(hk_state) if self.chars[FEATURE_PLAY_STOP]: hk_state = current_state == STATE_PLAYING @@ -200,8 +198,7 @@ class MediaPlayer(HomeAccessory): self.entity_id, hk_state, ) - if self.chars[FEATURE_PLAY_STOP].value != hk_state: - self.chars[FEATURE_PLAY_STOP].set_value(hk_state) + self.chars[FEATURE_PLAY_STOP].set_value(hk_state) if self.chars[FEATURE_TOGGLE_MUTE]: current_state = bool(new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED)) @@ -210,8 +207,7 @@ class MediaPlayer(HomeAccessory): self.entity_id, current_state, ) - if self.chars[FEATURE_TOGGLE_MUTE].value != current_state: - self.chars[FEATURE_TOGGLE_MUTE].set_value(current_state) + self.chars[FEATURE_TOGGLE_MUTE].set_value(current_state) @TYPES.register("TelevisionMediaPlayer") @@ -341,8 +337,7 @@ class TelevisionMediaPlayer(RemoteInputSelectAccessory): if current_state not in MEDIA_PLAYER_OFF_STATES: hk_state = 1 _LOGGER.debug("%s: Set current active state to %s", self.entity_id, hk_state) - if self.char_active.value != hk_state: - self.char_active.set_value(hk_state) + self.char_active.set_value(hk_state) # Set mute state if CHAR_VOLUME_SELECTOR in self.chars_speaker: @@ -352,7 +347,6 @@ class TelevisionMediaPlayer(RemoteInputSelectAccessory): self.entity_id, current_mute_state, ) - if self.char_mute.value != current_mute_state: - self.char_mute.set_value(current_mute_state) + self.char_mute.set_value(current_mute_state) self._async_update_input_state(hk_state, new_state) diff --git a/homeassistant/components/homekit/type_remotes.py b/homeassistant/components/homekit/type_remotes.py index 9e54221430c..53659adef77 100644 --- a/homeassistant/components/homekit/type_remotes.py +++ b/homeassistant/components/homekit/type_remotes.py @@ -154,8 +154,7 @@ class RemoteInputSelectAccessory(HomeAccessory): _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) - if self.char_input_source.value != index: - self.char_input_source.set_value(index) + self.char_input_source.set_value(index) return possible_sources = new_state.attributes.get(self.source_list_key, []) @@ -174,8 +173,7 @@ class RemoteInputSelectAccessory(HomeAccessory): source_name, possible_sources, ) - if self.char_input_source.value != 0: - self.char_input_source.set_value(0) + self.char_input_source.set_value(0) @TYPES.register("ActivityRemote") @@ -225,7 +223,6 @@ class ActivityRemote(RemoteInputSelectAccessory): # Power state remote hk_state = 1 if current_state == STATE_ON else 0 _LOGGER.debug("%s: Set current active state to %s", self.entity_id, hk_state) - if self.char_active.value != hk_state: - self.char_active.set_value(hk_state) + self.char_active.set_value(hk_state) self._async_update_input_state(hk_state, new_state) diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 6fe1a4e9e29..d76fbf0f534 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -158,15 +158,12 @@ class SecuritySystem(HomeAccessory): """Update security state after state changed.""" hass_state = new_state.state if (current_state := HASS_TO_HOMEKIT_CURRENT.get(hass_state)) is not None: - if self.char_current_state.value != current_state: - self.char_current_state.set_value(current_state) - _LOGGER.debug( - "%s: Updated current state to %s (%d)", - self.entity_id, - hass_state, - current_state, - ) - + self.char_current_state.set_value(current_state) + _LOGGER.debug( + "%s: Updated current state to %s (%d)", + self.entity_id, + hass_state, + current_state, + ) if (target_state := HASS_TO_HOMEKIT_TARGET.get(hass_state)) is not None: - if self.char_target_state.value != target_state: - self.char_target_state.set_value(target_state) + self.char_target_state.set_value(target_state) diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index b6cc4b05125..bcef7564fa3 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -101,11 +101,10 @@ class TemperatureSensor(HomeAccessory): temperature = convert_to_float(new_state.state) if temperature: temperature = temperature_to_homekit(temperature, unit) - if self.char_temp.value != temperature: - self.char_temp.set_value(temperature) - _LOGGER.debug( - "%s: Current temperature set to %.1f°C", self.entity_id, temperature - ) + self.char_temp.set_value(temperature) + _LOGGER.debug( + "%s: Current temperature set to %.1f°C", self.entity_id, temperature + ) @TYPES.register("HumiditySensor") @@ -128,7 +127,7 @@ class HumiditySensor(HomeAccessory): def async_update_state(self, new_state): """Update accessory after state change.""" humidity = convert_to_float(new_state.state) - if humidity and self.char_humidity.value != humidity: + if humidity: self.char_humidity.set_value(humidity) _LOGGER.debug("%s: Percent set to %d%%", self.entity_id, humidity) @@ -161,9 +160,8 @@ class AirQualitySensor(HomeAccessory): self.char_density.set_value(density) _LOGGER.debug("%s: Set density to %d", self.entity_id, density) air_quality = density_to_air_quality(density) - if self.char_quality.value != air_quality: - self.char_quality.set_value(air_quality) - _LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality) + self.char_quality.set_value(air_quality) + _LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality) @TYPES.register("CarbonMonoxideSensor") @@ -194,14 +192,12 @@ class CarbonMonoxideSensor(HomeAccessory): """Update accessory after state change.""" value = convert_to_float(new_state.state) if value: - if self.char_level.value != value: - self.char_level.set_value(value) + self.char_level.set_value(value) if value > self.char_peak.value: self.char_peak.set_value(value) co_detected = value > THRESHOLD_CO - if self.char_detected.value is not co_detected: - self.char_detected.set_value(co_detected) - _LOGGER.debug("%s: Set to %d", self.entity_id, value) + self.char_detected.set_value(co_detected) + _LOGGER.debug("%s: Set to %d", self.entity_id, value) @TYPES.register("CarbonDioxideSensor") @@ -232,14 +228,12 @@ class CarbonDioxideSensor(HomeAccessory): """Update accessory after state change.""" value = convert_to_float(new_state.state) if value: - if self.char_level.value != value: - self.char_level.set_value(value) + self.char_level.set_value(value) if value > self.char_peak.value: self.char_peak.set_value(value) co2_detected = value > THRESHOLD_CO2 - if self.char_detected.value is not co2_detected: - self.char_detected.set_value(co2_detected) - _LOGGER.debug("%s: Set to %d", self.entity_id, value) + self.char_detected.set_value(co2_detected) + _LOGGER.debug("%s: Set to %d", self.entity_id, value) @TYPES.register("LightSensor") @@ -262,7 +256,7 @@ class LightSensor(HomeAccessory): def async_update_state(self, new_state): """Update accessory after state change.""" luminance = convert_to_float(new_state.state) - if luminance and self.char_light.value != luminance: + if luminance: self.char_light.set_value(luminance) _LOGGER.debug("%s: Set to %d", self.entity_id, luminance) @@ -297,6 +291,5 @@ class BinarySensor(HomeAccessory): """Update accessory after state change.""" state = new_state.state detected = self.format(state in (STATE_ON, STATE_HOME)) - if self.char_detected.value != detected: - self.char_detected.set_value(detected) - _LOGGER.debug("%s: Set to %d", self.entity_id, detected) + self.char_detected.set_value(detected) + _LOGGER.debug("%s: Set to %d", self.entity_id, detected) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index ef9dadff287..3bb496a2abc 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -91,9 +91,8 @@ class Outlet(HomeAccessory): def async_update_state(self, new_state): """Update switch state after state changed.""" current_state = new_state.state == STATE_ON - if self.char_on.value is not current_state: - _LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state) - self.char_on.set_value(current_state) + _LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state) + self.char_on.set_value(current_state) @TYPES.register("Switch") @@ -123,8 +122,7 @@ class Switch(HomeAccessory): def reset_switch(self, *args): """Reset switch to emulate activate click.""" _LOGGER.debug("%s: Reset switch to off", self.entity_id) - if self.char_on.value is not False: - self.char_on.set_value(False) + self.char_on.set_value(False) def set_state(self, value): """Move switch state to value if call came from HomeKit.""" @@ -156,9 +154,8 @@ class Switch(HomeAccessory): return current_state = new_state.state == STATE_ON - if self.char_on.value is not current_state: - _LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state) - self.char_on.set_value(current_state) + _LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state) + self.char_on.set_value(current_state) @TYPES.register("Vacuum") @@ -186,9 +183,8 @@ class Vacuum(Switch): def async_update_state(self, new_state): """Update switch state after state changed.""" current_state = new_state.state in (STATE_CLEANING, STATE_ON) - if self.char_on.value is not current_state: - _LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state) - self.char_on.set_value(current_state) + _LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state) + self.char_on.set_value(current_state) @TYPES.register("Valve") @@ -226,9 +222,7 @@ class Valve(HomeAccessory): def async_update_state(self, new_state): """Update switch state after state changed.""" current_state = 1 if new_state.state == STATE_ON else 0 - if self.char_active.value != current_state: - _LOGGER.debug("%s: Set active state to %s", self.entity_id, current_state) - self.char_active.set_value(current_state) - if self.char_in_use.value != current_state: - _LOGGER.debug("%s: Set in_use state to %s", self.entity_id, current_state) - self.char_in_use.set_value(current_state) + _LOGGER.debug("%s: Set active state to %s", self.entity_id, current_state) + self.char_active.set_value(current_state) + _LOGGER.debug("%s: Set in_use state to %s", self.entity_id, current_state) + self.char_in_use.set_value(current_state) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index fb3063704c2..c36a32b0d5b 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -446,8 +446,7 @@ class Thermostat(HomeAccessory): if hvac_mode and hvac_mode in HC_HASS_TO_HOMEKIT: homekit_hvac_mode = HC_HASS_TO_HOMEKIT[hvac_mode] if homekit_hvac_mode in self.hc_homekit_to_hass: - if self.char_target_heat_cool.value != homekit_hvac_mode: - self.char_target_heat_cool.set_value(homekit_hvac_mode) + self.char_target_heat_cool.set_value(homekit_hvac_mode) else: _LOGGER.error( "Cannot map hvac target mode: %s to homekit as only %s modes are supported", @@ -459,30 +458,23 @@ class Thermostat(HomeAccessory): hvac_action = new_state.attributes.get(ATTR_HVAC_ACTION) if hvac_action: homekit_hvac_action = HC_HASS_TO_HOMEKIT_ACTION[hvac_action] - if self.char_current_heat_cool.value != homekit_hvac_action: - self.char_current_heat_cool.set_value(homekit_hvac_action) + self.char_current_heat_cool.set_value(homekit_hvac_action) # Update current temperature current_temp = _get_current_temperature(new_state, self._unit) - if current_temp is not None and self.char_current_temp.value != current_temp: + if current_temp is not None: self.char_current_temp.set_value(current_temp) # Update current humidity if CHAR_CURRENT_HUMIDITY in self.chars: current_humdity = new_state.attributes.get(ATTR_CURRENT_HUMIDITY) - if ( - isinstance(current_humdity, (int, float)) - and self.char_current_humidity.value != current_humdity - ): + if isinstance(current_humdity, (int, float)): self.char_current_humidity.set_value(current_humdity) # Update target humidity if CHAR_TARGET_HUMIDITY in self.chars: target_humdity = new_state.attributes.get(ATTR_HUMIDITY) - if ( - isinstance(target_humdity, (int, float)) - and self.char_target_humidity.value != target_humdity - ): + if isinstance(target_humdity, (int, float)): self.char_target_humidity.set_value(target_humdity) # Update cooling threshold temperature if characteristic exists @@ -490,16 +482,14 @@ class Thermostat(HomeAccessory): cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH) if isinstance(cooling_thresh, (int, float)): cooling_thresh = self._temperature_to_homekit(cooling_thresh) - if self.char_heating_thresh_temp.value != cooling_thresh: - self.char_cooling_thresh_temp.set_value(cooling_thresh) + self.char_cooling_thresh_temp.set_value(cooling_thresh) # Update heating threshold temperature if characteristic exists if self.char_heating_thresh_temp: heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW) if isinstance(heating_thresh, (int, float)): heating_thresh = self._temperature_to_homekit(heating_thresh) - if self.char_heating_thresh_temp.value != heating_thresh: - self.char_heating_thresh_temp.set_value(heating_thresh) + self.char_heating_thresh_temp.set_value(heating_thresh) # Update target temperature target_temp = _get_target_temperature(new_state, self._unit) @@ -515,14 +505,13 @@ class Thermostat(HomeAccessory): temp_high = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH) if isinstance(temp_high, (int, float)): target_temp = self._temperature_to_homekit(temp_high) - if target_temp and self.char_target_temp.value != target_temp: + if target_temp: self.char_target_temp.set_value(target_temp) # Update display units if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT: unit = UNIT_HASS_TO_HOMEKIT[self._unit] - if self.char_display_units.value != unit: - self.char_display_units.set_value(unit) + self.char_display_units.set_value(unit) @TYPES.register("WaterHeater") @@ -580,7 +569,7 @@ class WaterHeater(HomeAccessory): """Change operation mode to value if call came from HomeKit.""" _LOGGER.debug("%s: Set heat-cool to %d", self.entity_id, value) hass_value = HC_HOMEKIT_TO_HASS[value] - if hass_value != HVAC_MODE_HEAT and self.char_target_heat_cool.value != 1: + if hass_value != HVAC_MODE_HEAT: self.char_target_heat_cool.set_value(1) # Heat def set_target_temperature(self, value): @@ -600,28 +589,21 @@ class WaterHeater(HomeAccessory): """Update water_heater state after state change.""" # Update current and target temperature target_temperature = _get_target_temperature(new_state, self._unit) - if ( - target_temperature is not None - and target_temperature != self.char_target_temp.value - ): + if target_temperature is not None: self.char_target_temp.set_value(target_temperature) current_temperature = _get_current_temperature(new_state, self._unit) - if ( - current_temperature is not None - and current_temperature != self.char_current_temp.value - ): + if current_temperature is not None: self.char_current_temp.set_value(current_temperature) # Update display units if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT: unit = UNIT_HASS_TO_HOMEKIT[self._unit] - if self.char_display_units.value != unit: - self.char_display_units.set_value(unit) + self.char_display_units.set_value(unit) # Update target operation mode operation_mode = new_state.state - if operation_mode and self.char_target_heat_cool.value != 1: + if operation_mode: self.char_target_heat_cool.set_value(1) # Heat From f1c244e914265510867ea0f5e8d1bedb232c2371 Mon Sep 17 00:00:00 2001 From: dailow Date: Mon, 9 Aug 2021 14:50:09 -0700 Subject: [PATCH 087/355] Fix aqualogic state attribute update (#54354) --- homeassistant/components/aqualogic/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py index 01f31757c9d..fff73cf00fa 100644 --- a/homeassistant/components/aqualogic/sensor.py +++ b/homeassistant/components/aqualogic/sensor.py @@ -93,9 +93,10 @@ class AquaLogicSensor(SensorEntity): if panel is not None: if panel.is_metric: self._attr_unit_of_measurement = SENSOR_TYPES[self._type][1][0] - self._attr_state = getattr(panel, self._type) - self.async_write_ha_state() else: self._attr_unit_of_measurement = SENSOR_TYPES[self._type][1][1] + + self._attr_state = getattr(panel, self._type) + self.async_write_ha_state() else: self._attr_unit_of_measurement = None From 1cd575df533f60712b1dfe54f078ec25afd0f072 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 9 Aug 2021 14:50:39 -0700 Subject: [PATCH 088/355] Cast SimpliSafe version number as a string in device info (#54356) --- homeassistant/components/simplisafe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 0853aa3974c..924cf398f64 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -431,7 +431,7 @@ class SimpliSafeEntity(CoordinatorEntity): self._attr_device_info = { "identifiers": {(DOMAIN, system.system_id)}, "manufacturer": "SimpliSafe", - "model": system.version, + "model": str(system.version), "name": name, "via_device": (DOMAIN, system.serial), } From b42ac8b48f7ea5c91fd11293dc53456f31323cc0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 9 Aug 2021 23:53:39 +0200 Subject: [PATCH 089/355] Use EntityDescription - growatt_server (#54316) --- .../components/growatt_server/sensor.py | 1149 +++++++++-------- 1 file changed, 633 insertions(+), 516 deletions(-) diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 530842fab7b..6eb225e7535 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -1,4 +1,7 @@ """Read status of growatt inverters.""" +from __future__ import annotations + +from dataclasses import dataclass import datetime import json import logging @@ -6,7 +9,7 @@ import re import growattServer -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, @@ -37,514 +40,643 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = datetime.timedelta(minutes=1) -# Sensor type order is: Sensor name, Unit of measurement, api data name, additional options -TOTAL_SENSOR_TYPES = { - "total_money_today": ("Total money today", CURRENCY_EURO, "plantMoneyText", {}), - "total_money_total": ("Money lifetime", CURRENCY_EURO, "totalMoneyText", {}), - "total_energy_today": ( - "Energy Today", - ENERGY_KILO_WATT_HOUR, - "todayEnergy", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "total_output_power": ( - "Output Power", - POWER_WATT, - "invTodayPpv", - {"device_class": DEVICE_CLASS_POWER}, - ), - "total_energy_output": ( - "Lifetime energy output", - ENERGY_KILO_WATT_HOUR, - "totalEnergy", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "total_maximum_output": ( - "Maximum power", - POWER_WATT, - "nominalPower", - {"device_class": DEVICE_CLASS_POWER}, - ), -} -INVERTER_SENSOR_TYPES = { - "inverter_energy_today": ( - "Energy today", - ENERGY_KILO_WATT_HOUR, - "powerToday", - {"round": 1, "device_class": DEVICE_CLASS_ENERGY}, - ), - "inverter_energy_total": ( - "Lifetime energy output", - ENERGY_KILO_WATT_HOUR, - "powerTotal", - {"round": 1, "device_class": DEVICE_CLASS_ENERGY}, - ), - "inverter_voltage_input_1": ( - "Input 1 voltage", - ELECTRIC_POTENTIAL_VOLT, - "vpv1", - {"round": 2, "device_class": DEVICE_CLASS_VOLTAGE}, - ), - "inverter_amperage_input_1": ( - "Input 1 Amperage", - ELECTRIC_CURRENT_AMPERE, - "ipv1", - {"round": 1, "device_class": DEVICE_CLASS_CURRENT}, - ), - "inverter_wattage_input_1": ( - "Input 1 Wattage", - POWER_WATT, - "ppv1", - {"device_class": DEVICE_CLASS_POWER, "round": 1}, - ), - "inverter_voltage_input_2": ( - "Input 2 voltage", - ELECTRIC_POTENTIAL_VOLT, - "vpv2", - {"round": 1, "device_class": DEVICE_CLASS_VOLTAGE}, - ), - "inverter_amperage_input_2": ( - "Input 2 Amperage", - ELECTRIC_CURRENT_AMPERE, - "ipv2", - {"round": 1, "device_class": DEVICE_CLASS_CURRENT}, - ), - "inverter_wattage_input_2": ( - "Input 2 Wattage", - POWER_WATT, - "ppv2", - {"device_class": DEVICE_CLASS_POWER, "round": 1}, - ), - "inverter_voltage_input_3": ( - "Input 3 voltage", - ELECTRIC_POTENTIAL_VOLT, - "vpv3", - {"round": 1, "device_class": DEVICE_CLASS_VOLTAGE}, - ), - "inverter_amperage_input_3": ( - "Input 3 Amperage", - ELECTRIC_CURRENT_AMPERE, - "ipv3", - {"round": 1, "device_class": DEVICE_CLASS_CURRENT}, - ), - "inverter_wattage_input_3": ( - "Input 3 Wattage", - POWER_WATT, - "ppv3", - {"device_class": DEVICE_CLASS_POWER, "round": 1}, - ), - "inverter_internal_wattage": ( - "Internal wattage", - POWER_WATT, - "ppv", - {"device_class": DEVICE_CLASS_POWER, "round": 1}, - ), - "inverter_reactive_voltage": ( - "Reactive voltage", - ELECTRIC_POTENTIAL_VOLT, - "vacr", - {"round": 1, "device_class": DEVICE_CLASS_VOLTAGE}, - ), - "inverter_inverter_reactive_amperage": ( - "Reactive amperage", - ELECTRIC_CURRENT_AMPERE, - "iacr", - {"round": 1, "device_class": DEVICE_CLASS_CURRENT}, - ), - "inverter_frequency": ("AC frequency", FREQUENCY_HERTZ, "fac", {"round": 1}), - "inverter_current_wattage": ( - "Output power", - POWER_WATT, - "pac", - {"device_class": DEVICE_CLASS_POWER, "round": 1}, - ), - "inverter_current_reactive_wattage": ( - "Reactive wattage", - POWER_WATT, - "pacr", - {"device_class": DEVICE_CLASS_POWER, "round": 1}, - ), - "inverter_ipm_temperature": ( - "Intelligent Power Management temperature", - TEMP_CELSIUS, - "ipmTemperature", - {"device_class": DEVICE_CLASS_TEMPERATURE, "round": 1}, - ), - "inverter_temperature": ( - "Temperature", - TEMP_CELSIUS, - "temperature", - {"device_class": DEVICE_CLASS_TEMPERATURE, "round": 1}, - ), -} +@dataclass +class GrowattRequiredKeysMixin: + """Mixin for required keys.""" -STORAGE_SENSOR_TYPES = { - "storage_storage_production_today": ( - "Storage production today", - ENERGY_KILO_WATT_HOUR, - "eBatDisChargeToday", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "storage_storage_production_lifetime": ( - "Lifetime Storage production", - ENERGY_KILO_WATT_HOUR, - "eBatDisChargeTotal", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "storage_grid_discharge_today": ( - "Grid discharged today", - ENERGY_KILO_WATT_HOUR, - "eacDisChargeToday", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "storage_load_consumption_today": ( - "Load consumption today", - ENERGY_KILO_WATT_HOUR, - "eopDischrToday", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "storage_load_consumption_lifetime": ( - "Lifetime load consumption", - ENERGY_KILO_WATT_HOUR, - "eopDischrTotal", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "storage_grid_charged_today": ( - "Grid charged today", - ENERGY_KILO_WATT_HOUR, - "eacChargeToday", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "storage_charge_storage_lifetime": ( - "Lifetime storaged charged", - ENERGY_KILO_WATT_HOUR, - "eChargeTotal", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "storage_solar_production": ( - "Solar power production", - POWER_WATT, - "ppv", - {"device_class": DEVICE_CLASS_POWER}, - ), - "storage_battery_percentage": ( - "Battery percentage", - PERCENTAGE, - "capacity", - {"device_class": DEVICE_CLASS_BATTERY}, - ), - "storage_power_flow": ( - "Storage charging/ discharging(-ve)", - POWER_WATT, - "pCharge", - {"device_class": DEVICE_CLASS_POWER}, - ), - "storage_load_consumption_solar_storage": ( - "Load consumption(Solar + Storage)", - "VA", - "rateVA", - {}, - ), - "storage_charge_today": ( - "Charge today", - ENERGY_KILO_WATT_HOUR, - "eChargeToday", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "storage_import_from_grid": ( - "Import from grid", - POWER_WATT, - "pAcInPut", - {"device_class": DEVICE_CLASS_POWER}, - ), - "storage_import_from_grid_today": ( - "Import from grid today", - ENERGY_KILO_WATT_HOUR, - "eToUserToday", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "storage_import_from_grid_total": ( - "Import from grid total", - ENERGY_KILO_WATT_HOUR, - "eToUserTotal", - {"device_class": DEVICE_CLASS_ENERGY}, - ), - "storage_load_consumption": ( - "Load consumption", - POWER_WATT, - "outPutPower", - {"device_class": DEVICE_CLASS_POWER}, - ), - "storage_grid_voltage": ( - "AC input voltage", - ELECTRIC_POTENTIAL_VOLT, - "vGrid", - {"round": 2, "device_class": DEVICE_CLASS_VOLTAGE}, - ), - "storage_pv_charging_voltage": ( - "PV charging voltage", - ELECTRIC_POTENTIAL_VOLT, - "vpv", - {"round": 2, "device_class": DEVICE_CLASS_VOLTAGE}, - ), - "storage_ac_input_frequency_out": ( - "AC input frequency", - FREQUENCY_HERTZ, - "freqOutPut", - {"round": 2}, - ), - "storage_output_voltage": ( - "Output voltage", - ELECTRIC_POTENTIAL_VOLT, - "outPutVolt", - {"round": 2, "device_class": DEVICE_CLASS_VOLTAGE}, - ), - "storage_ac_output_frequency": ( - "Ac output frequency", - FREQUENCY_HERTZ, - "freqGrid", - {"round": 2}, - ), - "storage_current_PV": ( - "Solar charge current", - ELECTRIC_CURRENT_AMPERE, - "iAcCharge", - {"round": 2, "device_class": DEVICE_CLASS_CURRENT}, - ), - "storage_current_1": ( - "Solar current to storage", - ELECTRIC_CURRENT_AMPERE, - "iChargePV1", - {"round": 2, "device_class": DEVICE_CLASS_CURRENT}, - ), - "storage_grid_amperage_input": ( - "Grid charge current", - ELECTRIC_CURRENT_AMPERE, - "chgCurr", - {"round": 2, "device_class": DEVICE_CLASS_CURRENT}, - ), - "storage_grid_out_current": ( - "Grid out current", - ELECTRIC_CURRENT_AMPERE, - "outPutCurrent", - {"round": 2, "device_class": DEVICE_CLASS_CURRENT}, - ), - "storage_battery_voltage": ( - "Battery voltage", - ELECTRIC_POTENTIAL_VOLT, - "vBat", - {"round": 2, "device_class": DEVICE_CLASS_VOLTAGE}, - ), - "storage_load_percentage": ( - "Load percentage", - PERCENTAGE, - "loadPercent", - {"device_class": DEVICE_CLASS_BATTERY, "round": 2}, - ), -} + api_key: str -MIX_SENSOR_TYPES = { + +@dataclass +class GrowattSensorEntityDescription(SensorEntityDescription, GrowattRequiredKeysMixin): + """Describes Growatt sensor entity.""" + + precision: int | None = None + + +TOTAL_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( + GrowattSensorEntityDescription( + key="total_money_today", + name="Total money today", + api_key="plantMoneyText", + unit_of_measurement=CURRENCY_EURO, + ), + GrowattSensorEntityDescription( + key="total_money_total", + name="Money lifetime", + api_key="totalMoneyText", + unit_of_measurement=CURRENCY_EURO, + ), + GrowattSensorEntityDescription( + key="total_energy_today", + name="Energy Today", + api_key="todayEnergy", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="total_output_power", + name="Output Power", + api_key="invTodayPpv", + unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="total_energy_output", + name="Lifetime energy output", + api_key="totalEnergy", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="total_maximum_output", + name="Maximum power", + api_key="nominalPower", + unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + ), +) + +INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( + GrowattSensorEntityDescription( + key="inverter_energy_today", + name="Energy today", + api_key="powerToday", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_energy_total", + name="Lifetime energy output", + api_key="powerTotal", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_voltage_input_1", + name="Input 1 voltage", + api_key="vpv1", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=2, + ), + GrowattSensorEntityDescription( + key="inverter_amperage_input_1", + name="Input 1 Amperage", + api_key="ipv1", + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_wattage_input_1", + name="Input 1 Wattage", + api_key="ppv1", + unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_voltage_input_2", + name="Input 2 voltage", + api_key="vpv2", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_amperage_input_2", + name="Input 2 Amperage", + api_key="ipv2", + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_wattage_input_2", + name="Input 2 Wattage", + api_key="ppv2", + unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_voltage_input_3", + name="Input 3 voltage", + api_key="vpv3", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_amperage_input_3", + name="Input 3 Amperage", + api_key="ipv3", + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_wattage_input_3", + name="Input 3 Wattage", + api_key="ppv3", + unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_internal_wattage", + name="Internal wattage", + api_key="ppv", + unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_reactive_voltage", + name="Reactive voltage", + api_key="vacr", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_inverter_reactive_amperage", + name="Reactive amperage", + api_key="iacr", + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_frequency", + name="AC frequency", + api_key="fac", + unit_of_measurement=FREQUENCY_HERTZ, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_current_wattage", + name="Output power", + api_key="pac", + unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_current_reactive_wattage", + name="Reactive wattage", + api_key="pacr", + unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_ipm_temperature", + name="Intelligent Power Management temperature", + api_key="ipmTemperature", + unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + precision=1, + ), + GrowattSensorEntityDescription( + key="inverter_temperature", + name="Temperature", + api_key="temperature", + unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + precision=1, + ), +) + +STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( + GrowattSensorEntityDescription( + key="storage_storage_production_today", + name="Storage production today", + api_key="eBatDisChargeToday", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_storage_production_lifetime", + name="Lifetime Storage production", + api_key="eBatDisChargeTotal", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_grid_discharge_today", + name="Grid discharged today", + api_key="eacDisChargeToday", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_load_consumption_today", + name="Load consumption today", + api_key="eopDischrToday", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_load_consumption_lifetime", + name="Lifetime load consumption", + api_key="eopDischrTotal", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_grid_charged_today", + name="Grid charged today", + api_key="eacChargeToday", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_charge_storage_lifetime", + name="Lifetime storaged charged", + api_key="eChargeTotal", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_solar_production", + name="Solar power production", + api_key="ppv", + unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="storage_battery_percentage", + name="Battery percentage", + api_key="capacity", + unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + ), + GrowattSensorEntityDescription( + key="storage_power_flow", + name="Storage charging/ discharging(-ve)", + api_key="pCharge", + unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="storage_load_consumption_solar_storage", + name="Load consumption(Solar + Storage)", + api_key="rateVA", + unit_of_measurement="VA", + ), + GrowattSensorEntityDescription( + key="storage_charge_today", + name="Charge today", + api_key="eChargeToday", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_import_from_grid", + name="Import from grid", + api_key="pAcInPut", + unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="storage_import_from_grid_today", + name="Import from grid today", + api_key="eToUserToday", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_import_from_grid_total", + name="Import from grid total", + api_key="eToUserTotal", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), + GrowattSensorEntityDescription( + key="storage_load_consumption", + name="Load consumption", + api_key="outPutPower", + unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + ), + GrowattSensorEntityDescription( + key="storage_grid_voltage", + name="AC input voltage", + api_key="vGrid", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_pv_charging_voltage", + name="PV charging voltage", + api_key="vpv", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_ac_input_frequency_out", + name="AC input frequency", + api_key="freqOutPut", + unit_of_measurement=FREQUENCY_HERTZ, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_output_voltage", + name="Output voltage", + api_key="outPutVolt", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_ac_output_frequency", + name="Ac output frequency", + api_key="freqGrid", + unit_of_measurement=FREQUENCY_HERTZ, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_current_PV", + name="Solar charge current", + api_key="iAcCharge", + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_current_1", + name="Solar current to storage", + api_key="iChargePV1", + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_grid_amperage_input", + name="Grid charge current", + api_key="chgCurr", + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_grid_out_current", + name="Grid out current", + api_key="outPutCurrent", + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_battery_voltage", + name="Battery voltage", + api_key="vBat", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + precision=2, + ), + GrowattSensorEntityDescription( + key="storage_load_percentage", + name="Load percentage", + api_key="loadPercent", + unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + precision=2, + ), +) + +MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( # Values from 'mix_info' API call - "mix_statement_of_charge": ( - "Statement of charge", - PERCENTAGE, - "capacity", - {"device_class": DEVICE_CLASS_BATTERY}, + GrowattSensorEntityDescription( + key="mix_statement_of_charge", + name="Statement of charge", + api_key="capacity", + unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, ), - "mix_battery_charge_today": ( - "Battery charged today", - ENERGY_KILO_WATT_HOUR, - "eBatChargeToday", - {"device_class": DEVICE_CLASS_ENERGY}, + GrowattSensorEntityDescription( + key="mix_battery_charge_today", + name="Battery charged today", + api_key="eBatChargeToday", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), - "mix_battery_charge_lifetime": ( - "Lifetime battery charged", - ENERGY_KILO_WATT_HOUR, - "eBatChargeTotal", - {"device_class": DEVICE_CLASS_ENERGY}, + GrowattSensorEntityDescription( + key="mix_battery_charge_lifetime", + name="Lifetime battery charged", + api_key="eBatChargeTotal", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), - "mix_battery_discharge_today": ( - "Battery discharged today", - ENERGY_KILO_WATT_HOUR, - "eBatDisChargeToday", - {"device_class": DEVICE_CLASS_ENERGY}, + GrowattSensorEntityDescription( + key="mix_battery_discharge_today", + name="Battery discharged today", + api_key="eBatDisChargeToday", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), - "mix_battery_discharge_lifetime": ( - "Lifetime battery discharged", - ENERGY_KILO_WATT_HOUR, - "eBatDisChargeTotal", - {"device_class": DEVICE_CLASS_ENERGY}, + GrowattSensorEntityDescription( + key="mix_battery_discharge_lifetime", + name="Lifetime battery discharged", + api_key="eBatDisChargeTotal", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), - "mix_solar_generation_today": ( - "Solar energy today", - ENERGY_KILO_WATT_HOUR, - "epvToday", - {"device_class": DEVICE_CLASS_ENERGY}, + GrowattSensorEntityDescription( + key="mix_solar_generation_today", + name="Solar energy today", + api_key="epvToday", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), - "mix_solar_generation_lifetime": ( - "Lifetime solar energy", - ENERGY_KILO_WATT_HOUR, - "epvTotal", - {"device_class": DEVICE_CLASS_ENERGY}, + GrowattSensorEntityDescription( + key="mix_solar_generation_lifetime", + name="Lifetime solar energy", + api_key="epvTotal", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), - "mix_battery_discharge_w": ( - "Battery discharging W", - POWER_WATT, - "pDischarge1", - {"device_class": DEVICE_CLASS_POWER}, + GrowattSensorEntityDescription( + key="mix_battery_discharge_w", + name="Battery discharging W", + api_key="pDischarge1", + unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, ), - "mix_battery_voltage": ( - "Battery voltage", - ELECTRIC_POTENTIAL_VOLT, - "vbat", - {"device_class": DEVICE_CLASS_VOLTAGE}, + GrowattSensorEntityDescription( + key="mix_battery_voltage", + name="Battery voltage", + api_key="vbat", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, ), - "mix_pv1_voltage": ( - "PV1 voltage", - ELECTRIC_POTENTIAL_VOLT, - "vpv1", - {"device_class": DEVICE_CLASS_VOLTAGE}, + GrowattSensorEntityDescription( + key="mix_pv1_voltage", + name="PV1 voltage", + api_key="vpv1", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, ), - "mix_pv2_voltage": ( - "PV2 voltage", - ELECTRIC_POTENTIAL_VOLT, - "vpv2", - {"device_class": DEVICE_CLASS_VOLTAGE}, + GrowattSensorEntityDescription( + key="mix_pv2_voltage", + name="PV2 voltage", + api_key="vpv2", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, ), # Values from 'mix_totals' API call - "mix_load_consumption_today": ( - "Load consumption today", - ENERGY_KILO_WATT_HOUR, - "elocalLoadToday", - {"device_class": DEVICE_CLASS_ENERGY}, + GrowattSensorEntityDescription( + key="mix_load_consumption_today", + name="Load consumption today", + api_key="elocalLoadToday", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), - "mix_load_consumption_lifetime": ( - "Lifetime load consumption", - ENERGY_KILO_WATT_HOUR, - "elocalLoadTotal", - {"device_class": DEVICE_CLASS_ENERGY}, + GrowattSensorEntityDescription( + key="mix_load_consumption_lifetime", + name="Lifetime load consumption", + api_key="elocalLoadTotal", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), - "mix_export_to_grid_today": ( - "Export to grid today", - ENERGY_KILO_WATT_HOUR, - "etoGridToday", - {"device_class": DEVICE_CLASS_ENERGY}, + GrowattSensorEntityDescription( + key="mix_export_to_grid_today", + name="Export to grid today", + api_key="etoGridToday", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), - "mix_export_to_grid_lifetime": ( - "Lifetime export to grid", - ENERGY_KILO_WATT_HOUR, - "etogridTotal", - {"device_class": DEVICE_CLASS_ENERGY}, + GrowattSensorEntityDescription( + key="mix_export_to_grid_lifetime", + name="Lifetime export to grid", + api_key="etogridTotal", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), # Values from 'mix_system_status' API call - "mix_battery_charge": ( - "Battery charging", - POWER_KILO_WATT, - "chargePower", - {"device_class": DEVICE_CLASS_POWER}, + GrowattSensorEntityDescription( + key="mix_battery_charge", + name="Battery charging", + api_key="chargePower", + unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, ), - "mix_load_consumption": ( - "Load consumption", - POWER_KILO_WATT, - "pLocalLoad", - {"device_class": DEVICE_CLASS_POWER}, + GrowattSensorEntityDescription( + key="mix_load_consumption", + name="Load consumption", + api_key="pLocalLoad", + unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, ), - "mix_wattage_pv_1": ( - "PV1 Wattage", - POWER_KILO_WATT, - "pPv1", - {"device_class": DEVICE_CLASS_POWER}, + GrowattSensorEntityDescription( + key="mix_wattage_pv_1", + name="PV1 Wattage", + api_key="pPv1", + unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, ), - "mix_wattage_pv_2": ( - "PV2 Wattage", - POWER_KILO_WATT, - "pPv2", - {"device_class": DEVICE_CLASS_POWER}, + GrowattSensorEntityDescription( + key="mix_wattage_pv_2", + name="PV2 Wattage", + api_key="pPv2", + unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, ), - "mix_wattage_pv_all": ( - "All PV Wattage", - POWER_KILO_WATT, - "ppv", - {"device_class": DEVICE_CLASS_POWER}, + GrowattSensorEntityDescription( + key="mix_wattage_pv_all", + name="All PV Wattage", + api_key="ppv", + unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, ), - "mix_export_to_grid": ( - "Export to grid", - POWER_KILO_WATT, - "pactogrid", - {"device_class": DEVICE_CLASS_POWER}, + GrowattSensorEntityDescription( + key="mix_export_to_grid", + name="Export to grid", + api_key="pactogrid", + unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, ), - "mix_import_from_grid": ( - "Import from grid", - POWER_KILO_WATT, - "pactouser", - {"device_class": DEVICE_CLASS_POWER}, + GrowattSensorEntityDescription( + key="mix_import_from_grid", + name="Import from grid", + api_key="pactouser", + unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, ), - "mix_battery_discharge_kw": ( - "Battery discharging kW", - POWER_KILO_WATT, - "pdisCharge1", - {"device_class": DEVICE_CLASS_POWER}, + GrowattSensorEntityDescription( + key="mix_battery_discharge_kw", + name="Battery discharging kW", + api_key="pdisCharge1", + unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, ), - "mix_grid_voltage": ( - "Grid voltage", - ELECTRIC_POTENTIAL_VOLT, - "vAc1", - {"device_class": DEVICE_CLASS_VOLTAGE}, + GrowattSensorEntityDescription( + key="mix_grid_voltage", + name="Grid voltage", + api_key="vAc1", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, ), # Values from 'mix_detail' API call - "mix_system_production_today": ( - "System production today (self-consumption + export)", - ENERGY_KILO_WATT_HOUR, - "eCharge", - {"device_class": DEVICE_CLASS_ENERGY}, + GrowattSensorEntityDescription( + key="mix_system_production_today", + name="System production today (self-consumption + export)", + api_key="eCharge", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), - "mix_load_consumption_solar_today": ( - "Load consumption today (solar)", - ENERGY_KILO_WATT_HOUR, - "eChargeToday", - {"device_class": DEVICE_CLASS_ENERGY}, + GrowattSensorEntityDescription( + key="mix_load_consumption_solar_today", + name="Load consumption today (solar)", + api_key="eChargeToday", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), - "mix_self_consumption_today": ( - "Self consumption today (solar + battery)", - ENERGY_KILO_WATT_HOUR, - "eChargeToday1", - {"device_class": DEVICE_CLASS_ENERGY}, + GrowattSensorEntityDescription( + key="mix_self_consumption_today", + name="Self consumption today (solar + battery)", + api_key="eChargeToday1", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), - "mix_load_consumption_battery_today": ( - "Load consumption today (battery)", - ENERGY_KILO_WATT_HOUR, - "echarge1", - {"device_class": DEVICE_CLASS_ENERGY}, + GrowattSensorEntityDescription( + key="mix_load_consumption_battery_today", + name="Load consumption today (battery)", + api_key="echarge1", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), - "mix_import_from_grid_today": ( - "Import from grid today (load)", - ENERGY_KILO_WATT_HOUR, - "etouser", - {"device_class": DEVICE_CLASS_ENERGY}, + GrowattSensorEntityDescription( + key="mix_import_from_grid_today", + name="Import from grid today (load)", + api_key="etouser", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), # This sensor is manually created using the most recent X-Axis value from the chartData - "mix_last_update": ( - "Last Data Update", - None, - "lastdataupdate", - {"device_class": DEVICE_CLASS_TIMESTAMP}, + GrowattSensorEntityDescription( + key="mix_last_update", + name="Last Data Update", + api_key="lastdataupdate", + unit_of_measurement=None, + device_class=DEVICE_CLASS_TIMESTAMP, ), # Values from 'dashboard_data' API call - "mix_import_from_grid_today_combined": ( - "Import from grid today (load + charging)", - ENERGY_KILO_WATT_HOUR, - "etouser_combined", # This id is not present in the raw API data, it is added by the sensor - {"device_class": DEVICE_CLASS_ENERGY}, + GrowattSensorEntityDescription( + key="mix_import_from_grid_today_combined", + 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 + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, ), -} - -SENSOR_TYPES = { - **TOTAL_SENSOR_TYPES, - **INVERTER_SENSOR_TYPES, - **STORAGE_SENSOR_TYPES, - **MIX_SENSOR_TYPES, -} +) def get_device_list(api, config): @@ -579,42 +711,48 @@ async def async_setup_entry(hass, config_entry, async_add_entities): devices, plant_id = await hass.async_add_executor_job(get_device_list, api, config) - entities = [] probe = GrowattData(api, username, password, plant_id, "total") - for sensor in TOTAL_SENSOR_TYPES: - entities.append( - GrowattInverter(probe, f"{name} Total", sensor, f"{plant_id}-{sensor}") + entities = [ + GrowattInverter( + probe, + name=f"{name} Total", + unique_id=f"{plant_id}-{description.key}", + description=description, ) + for description in TOTAL_SENSOR_TYPES + ] # Add sensors for each device in the specified plant. for device in devices: probe = GrowattData( api, username, password, device["deviceSn"], device["deviceType"] ) - sensors = [] + sensor_descriptions = () if device["deviceType"] == "inverter": - sensors = INVERTER_SENSOR_TYPES + sensor_descriptions = INVERTER_SENSOR_TYPES elif device["deviceType"] == "storage": probe.plant_id = plant_id - sensors = STORAGE_SENSOR_TYPES + sensor_descriptions = STORAGE_SENSOR_TYPES elif device["deviceType"] == "mix": probe.plant_id = plant_id - sensors = MIX_SENSOR_TYPES + sensor_descriptions = MIX_SENSOR_TYPES else: _LOGGER.debug( "Device type %s was found but is not supported right now", device["deviceType"], ) - for sensor in sensors: - entities.append( + entities.extend( + [ GrowattInverter( probe, - f"{device['deviceAilas']}", - sensor, - f"{device['deviceSn']}-{sensor}", + name=f"{device['deviceAilas']}", + unique_id=f"{device['deviceSn']}-{description.key}", + description=description, ) - ) + for description in sensor_descriptions + ] + ) async_add_entities(entities, True) @@ -622,48 +760,27 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class GrowattInverter(SensorEntity): """Representation of a Growatt Sensor.""" - def __init__(self, probe, name, sensor, unique_id): + entity_description: GrowattSensorEntityDescription + + def __init__( + self, probe, name, unique_id, description: GrowattSensorEntityDescription + ): """Initialize a PVOutput sensor.""" - self.sensor = sensor self.probe = probe - self._name = name - self._state = None - self._unique_id = unique_id + self.entity_description = description - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name} {SENSOR_TYPES[self.sensor][0]}" - - @property - def unique_id(self): - """Return the unique id of the sensor.""" - return self._unique_id - - @property - def icon(self): - """Return the icon of the sensor.""" - return "mdi:solar-power" + self._attr_name = f"{name} {description.name}" + self._attr_unique_id = unique_id + self._attr_icon = "mdi:solar-power" @property def state(self): """Return the state of the sensor.""" - result = self.probe.get_data(SENSOR_TYPES[self.sensor][2]) - round_to = SENSOR_TYPES[self.sensor][3].get("round") - if round_to is not None: - result = round(result, round_to) + result = self.probe.get_data(self.entity_description.api_key) + if self.entity_description.precision is not None: + result = round(result, self.entity_description.precision) return result - @property - def device_class(self): - """Return the device class of the sensor.""" - return SENSOR_TYPES[self.sensor][3].get("device_class") - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return SENSOR_TYPES[self.sensor][1] - def update(self): """Get the latest data from the Growat API and updates the state.""" self.probe.update() From aeb7a6c09058b035f5ba74d688ead78ae3826198 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 9 Aug 2021 23:54:07 +0200 Subject: [PATCH 090/355] Use EntityDescription - bitcoin (#54320) * Use EntityDescription - bitcoin * Remove default values --- homeassistant/components/bitcoin/sensor.py | 193 +++++++++++++++------ 1 file changed, 139 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index d11c2a2b726..29945bd56dc 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -1,11 +1,17 @@ """Bitcoin information service that uses blockchain.com.""" +from __future__ import annotations + from datetime import timedelta import logging from blockchain import exchangerates, statistics import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_CURRENCY, @@ -25,34 +31,112 @@ ICON = "mdi:currency-btc" SCAN_INTERVAL = timedelta(minutes=5) -OPTION_TYPES = { - "exchangerate": ["Exchange rate (1 BTC)", None], - "trade_volume_btc": ["Trade volume", "BTC"], - "miners_revenue_usd": ["Miners revenue", "USD"], - "btc_mined": ["Mined", "BTC"], - "trade_volume_usd": ["Trade volume", "USD"], - "difficulty": ["Difficulty", None], - "minutes_between_blocks": ["Time between Blocks", TIME_MINUTES], - "number_of_transactions": ["No. of Transactions", None], - "hash_rate": ["Hash rate", f"PH/{TIME_SECONDS}"], - "timestamp": ["Timestamp", None], - "mined_blocks": ["Mined Blocks", None], - "blocks_size": ["Block size", None], - "total_fees_btc": ["Total fees", "BTC"], - "total_btc_sent": ["Total sent", "BTC"], - "estimated_btc_sent": ["Estimated sent", "BTC"], - "total_btc": ["Total", "BTC"], - "total_blocks": ["Total Blocks", None], - "next_retarget": ["Next retarget", None], - "estimated_transaction_volume_usd": ["Est. Transaction volume", "USD"], - "miners_revenue_btc": ["Miners revenue", "BTC"], - "market_price_usd": ["Market price", "USD"], -} +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="exchangerate", + name="Exchange rate (1 BTC)", + ), + SensorEntityDescription( + key="trade_volume_btc", + name="Trade volume", + unit_of_measurement="BTC", + ), + SensorEntityDescription( + key="miners_revenue_usd", + name="Miners revenue", + unit_of_measurement="USD", + ), + SensorEntityDescription( + key="btc_mined", + name="Mined", + unit_of_measurement="BTC", + ), + SensorEntityDescription( + key="trade_volume_usd", + name="Trade volume", + unit_of_measurement="USD", + ), + SensorEntityDescription( + key="difficulty", + name="Difficulty", + ), + SensorEntityDescription( + key="minutes_between_blocks", + name="Time between Blocks", + unit_of_measurement=TIME_MINUTES, + ), + SensorEntityDescription( + key="number_of_transactions", + name="No. of Transactions", + ), + SensorEntityDescription( + key="hash_rate", + name="Hash rate", + unit_of_measurement=f"PH/{TIME_SECONDS}", + ), + SensorEntityDescription( + key="timestamp", + name="Timestamp", + ), + SensorEntityDescription( + key="mined_blocks", + name="Mined Blocks", + ), + SensorEntityDescription( + key="blocks_size", + name="Block size", + ), + SensorEntityDescription( + key="total_fees_btc", + name="Total fees", + unit_of_measurement="BTC", + ), + SensorEntityDescription( + key="total_btc_sent", + name="Total sent", + unit_of_measurement="BTC", + ), + SensorEntityDescription( + key="estimated_btc_sent", + name="Estimated sent", + unit_of_measurement="BTC", + ), + SensorEntityDescription( + key="total_btc", + name="Total", + unit_of_measurement="BTC", + ), + SensorEntityDescription( + key="total_blocks", + name="Total Blocks", + ), + SensorEntityDescription( + key="next_retarget", + name="Next retarget", + ), + SensorEntityDescription( + key="estimated_transaction_volume_usd", + name="Est. Transaction volume", + unit_of_measurement="USD", + ), + SensorEntityDescription( + key="miners_revenue_btc", + name="Miners revenue", + unit_of_measurement="BTC", + ), + SensorEntityDescription( + key="market_price_usd", + name="Market price", + unit_of_measurement="USD", + ), +) + +OPTION_KEYS = [desc.key for desc in SENSOR_TYPES] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_DISPLAY_OPTIONS, default=[]): vol.All( - cv.ensure_list, [vol.In(OPTION_TYPES)] + cv.ensure_list, [vol.In(OPTION_KEYS)] ), vol.Optional(CONF_CURRENCY, default=DEFAULT_CURRENCY): cv.string, } @@ -69,11 +153,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): currency = DEFAULT_CURRENCY data = BitcoinData() - dev = [] - for variable in config[CONF_DISPLAY_OPTIONS]: - dev.append(BitcoinSensor(data, variable, currency)) + entities = [ + BitcoinSensor(data, currency, description) + for description in SENSOR_TYPES + if description.key in config[CONF_DISPLAY_OPTIONS] + ] - add_entities(dev, True) + add_entities(entities, True) class BitcoinSensor(SensorEntity): @@ -82,13 +168,11 @@ class BitcoinSensor(SensorEntity): _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_icon = ICON - def __init__(self, data, option_type, currency): + def __init__(self, data, currency, description: SensorEntityDescription): """Initialize the sensor.""" + self.entity_description = description self.data = data - self._attr_name = OPTION_TYPES[option_type][0] - self._attr_unit_of_measurement = OPTION_TYPES[option_type][1] self._currency = currency - self.type = option_type def update(self): """Get the latest data and updates the states.""" @@ -96,48 +180,49 @@ class BitcoinSensor(SensorEntity): stats = self.data.stats ticker = self.data.ticker - if self.type == "exchangerate": + sensor_type = self.entity_description.key + if sensor_type == "exchangerate": self._attr_state = ticker[self._currency].p15min self._attr_unit_of_measurement = self._currency - elif self.type == "trade_volume_btc": + elif sensor_type == "trade_volume_btc": self._attr_state = f"{stats.trade_volume_btc:.1f}" - elif self.type == "miners_revenue_usd": + elif sensor_type == "miners_revenue_usd": self._attr_state = f"{stats.miners_revenue_usd:.0f}" - elif self.type == "btc_mined": + elif sensor_type == "btc_mined": self._attr_state = str(stats.btc_mined * 0.00000001) - elif self.type == "trade_volume_usd": + elif sensor_type == "trade_volume_usd": self._attr_state = f"{stats.trade_volume_usd:.1f}" - elif self.type == "difficulty": + elif sensor_type == "difficulty": self._attr_state = f"{stats.difficulty:.0f}" - elif self.type == "minutes_between_blocks": + elif sensor_type == "minutes_between_blocks": self._attr_state = f"{stats.minutes_between_blocks:.2f}" - elif self.type == "number_of_transactions": + elif sensor_type == "number_of_transactions": self._attr_state = str(stats.number_of_transactions) - elif self.type == "hash_rate": + elif sensor_type == "hash_rate": self._attr_state = f"{stats.hash_rate * 0.000001:.1f}" - elif self.type == "timestamp": + elif sensor_type == "timestamp": self._attr_state = stats.timestamp - elif self.type == "mined_blocks": + elif sensor_type == "mined_blocks": self._attr_state = str(stats.mined_blocks) - elif self.type == "blocks_size": + elif sensor_type == "blocks_size": self._attr_state = f"{stats.blocks_size:.1f}" - elif self.type == "total_fees_btc": + elif sensor_type == "total_fees_btc": self._attr_state = f"{stats.total_fees_btc * 0.00000001:.2f}" - elif self.type == "total_btc_sent": + elif sensor_type == "total_btc_sent": self._attr_state = f"{stats.total_btc_sent * 0.00000001:.2f}" - elif self.type == "estimated_btc_sent": + elif sensor_type == "estimated_btc_sent": self._attr_state = f"{stats.estimated_btc_sent * 0.00000001:.2f}" - elif self.type == "total_btc": + elif sensor_type == "total_btc": self._attr_state = f"{stats.total_btc * 0.00000001:.2f}" - elif self.type == "total_blocks": + elif sensor_type == "total_blocks": self._attr_state = f"{stats.total_blocks:.0f}" - elif self.type == "next_retarget": + elif sensor_type == "next_retarget": self._attr_state = f"{stats.next_retarget:.2f}" - elif self.type == "estimated_transaction_volume_usd": + elif sensor_type == "estimated_transaction_volume_usd": self._attr_state = f"{stats.estimated_transaction_volume_usd:.2f}" - elif self.type == "miners_revenue_btc": + elif sensor_type == "miners_revenue_btc": self._attr_state = f"{stats.miners_revenue_btc * 0.00000001:.1f}" - elif self.type == "market_price_usd": + elif sensor_type == "market_price_usd": self._attr_state = f"{stats.market_price_usd:.2f}" From 4459d8674abc07d5c0f8c84d3af23dc9d1eabd74 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 9 Aug 2021 23:56:33 +0200 Subject: [PATCH 091/355] Add `binary_sensor` platform for Xiaomi Miio integration (#54096) --- .coveragerc | 1 + .../components/xiaomi_miio/__init__.py | 9 +- .../components/xiaomi_miio/binary_sensor.py | 87 +++++++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/xiaomi_miio/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 839bd34f6e5..5d9c5e9c5c8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1202,6 +1202,7 @@ omit = homeassistant/components/xiaomi_miio/__init__.py homeassistant/components/xiaomi_miio/air_quality.py homeassistant/components/xiaomi_miio/alarm_control_panel.py + homeassistant/components/xiaomi_miio/binary_sensor.py homeassistant/components/xiaomi_miio/device.py homeassistant/components/xiaomi_miio/device_tracker.py homeassistant/components/xiaomi_miio/fan.py diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 89355ae309e..bd9e69bd12d 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -37,7 +37,14 @@ _LOGGER = logging.getLogger(__name__) GATEWAY_PLATFORMS = ["alarm_control_panel", "light", "sensor", "switch"] SWITCH_PLATFORMS = ["switch"] FAN_PLATFORMS = ["fan"] -HUMIDIFIER_PLATFORMS = ["humidifier", "number", "select", "sensor", "switch"] +HUMIDIFIER_PLATFORMS = [ + "binary_sensor", + "humidifier", + "number", + "select", + "sensor", + "switch", +] LIGHT_PLATFORMS = ["light"] VACUUM_PLATFORMS = ["vacuum"] AIR_MONITOR_PLATFORMS = ["air_quality", "sensor"] diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py new file mode 100644 index 00000000000..c2f14b17d22 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -0,0 +1,87 @@ +"""Support for Xiaomi Miio binary sensors.""" +from enum import Enum + +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, + BinarySensorEntityDescription, +) + +from .const import ( + CONF_DEVICE, + CONF_FLOW_TYPE, + CONF_MODEL, + DOMAIN, + KEY_COORDINATOR, + KEY_DEVICE, + MODELS_HUMIDIFIER_MJJSQ, +) +from .device import XiaomiCoordinatedMiioEntity + +ATTR_NO_WATER = "no_water" +ATTR_WATER_TANK_DETACHED = "water_tank_detached" + +BINARY_SENSOR_TYPES = ( + BinarySensorEntityDescription( + key=ATTR_NO_WATER, + name="Water Tank Empty", + icon="mdi:water-off-outline", + ), + BinarySensorEntityDescription( + key=ATTR_WATER_TANK_DETACHED, + name="Water Tank Detached", + icon="mdi:flask-empty-off-outline", + ), +) + +HUMIDIFIER_MJJSQ_BINARY_SENSORS = (ATTR_NO_WATER, ATTR_WATER_TANK_DETACHED) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Xiaomi sensor from a config entry.""" + entities = [] + + if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: + model = config_entry.data[CONF_MODEL] + sensors = [] + if model in MODELS_HUMIDIFIER_MJJSQ: + sensors = HUMIDIFIER_MJJSQ_BINARY_SENSORS + for description in BINARY_SENSOR_TYPES: + if description.key not in sensors: + continue + entities.append( + XiaomiGenericBinarySensor( + f"{config_entry.title} {description.name}", + hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE], + config_entry, + f"{description.key}_{config_entry.unique_id}", + hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR], + description, + ) + ) + + async_add_entities(entities) + + +class XiaomiGenericBinarySensor(XiaomiCoordinatedMiioEntity, BinarySensorEntity): + """Representation of a Xiaomi Humidifier binary sensor.""" + + def __init__(self, name, device, entry, unique_id, coordinator, description): + """Initialize the entity.""" + super().__init__(name, device, entry, unique_id, coordinator) + + self.entity_description = description + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._extract_value_from_attribute( + self.coordinator.data, self.entity_description.key + ) + + @staticmethod + def _extract_value_from_attribute(state, attribute): + value = getattr(state, attribute) + if isinstance(value, Enum): + return value.value + + return value From d4a3d0462d08932c48fc0cd39f5c8f3689ca98d6 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Mon, 9 Aug 2021 15:11:21 -0700 Subject: [PATCH 092/355] Minor motionEye readability improvement (#54251) --- homeassistant/components/motioneye/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index acafdceeb05..3eebcd4ee53 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -237,8 +237,8 @@ def _add_camera( if entry.options.get(CONF_WEBHOOK_SET, DEFAULT_WEBHOOK_SET): url = async_generate_motioneye_webhook(hass, entry.data[CONF_WEBHOOK_ID]) - if url and ( - _set_webhook( + if url: + set_motion_event = _set_webhook( _build_url( device, url, @@ -250,7 +250,8 @@ def _add_camera( KEY_WEB_HOOK_NOTIFICATIONS_ENABLED, camera, ) - | _set_webhook( + + set_storage_event = _set_webhook( _build_url( device, url, @@ -262,8 +263,8 @@ def _add_camera( KEY_WEB_HOOK_STORAGE_ENABLED, camera, ) - ): - hass.async_create_task(client.async_set_camera(camera_id, camera)) + if set_motion_event or set_storage_event: + hass.async_create_task(client.async_set_camera(camera_id, camera)) async_dispatcher_send( hass, From 33c33d844eefc89adfcf96a3c46834b7b2f9012b Mon Sep 17 00:00:00 2001 From: Matthew LeMay Date: Mon, 9 Aug 2021 18:16:33 -0400 Subject: [PATCH 093/355] Update pyupgrade to 2.23.3 (#54179) --- .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 1e36ae652d6..b31a9cef116 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.23.0 + rev: v2.23.3 hooks: - id: pyupgrade args: [--py38-plus] diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 795a4c3bcd6..19d55b1255c 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -12,5 +12,5 @@ mccabe==0.6.1 pycodestyle==2.7.0 pydocstyle==6.0.0 pyflakes==2.3.1 -pyupgrade==2.23.0 +pyupgrade==2.23.3 yamllint==1.26.1 From 3184f0697f30b2e141c420f736fd66d127d32aef Mon Sep 17 00:00:00 2001 From: "Richard T. Schaefer" Date: Mon, 9 Aug 2021 17:38:56 -0500 Subject: [PATCH 094/355] Add Save Persistent States service (#53881) --- .../components/homeassistant/__init__.py | 11 +++- .../components/homeassistant/services.yaml | 6 ++ homeassistant/const.py | 1 + homeassistant/helpers/restore_state.py | 6 ++ tests/components/homeassistant/test_init.py | 16 ++++++ tests/helpers/test_restore_state.py | 56 +++++++++++++++++++ 6 files changed, 95 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index e798fda209b..d21cd1359f1 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -14,13 +14,14 @@ from homeassistant.const import ( RESTART_EXIT_CODE, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, + SERVICE_SAVE_PERSISTENT_STATES, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, ) import homeassistant.core as ha from homeassistant.exceptions import HomeAssistantError, Unauthorized, UnknownUser -from homeassistant.helpers import config_validation as cv, recorder +from homeassistant.helpers import config_validation as cv, recorder, restore_state from homeassistant.helpers.service import ( async_extract_config_entry_ids, async_extract_referenced_entity_ids, @@ -53,6 +54,10 @@ SHUTDOWN_SERVICES = (SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART) async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: # noqa: C901 """Set up general services related to Home Assistant.""" + async def async_save_persistent_states(service): + """Handle calls to homeassistant.save_persistent_states.""" + await restore_state.RestoreStateData.async_save_persistent_states(hass) + async def async_handle_turn_service(service): """Handle calls to homeassistant.turn_on/off.""" referenced = await async_extract_referenced_entity_ids(hass, service) @@ -114,6 +119,10 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: # noqa: C9 if tasks: await asyncio.gather(*tasks) + hass.services.async_register( + ha.DOMAIN, SERVICE_SAVE_PERSISTENT_STATES, async_save_persistent_states + ) + service_schema = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids}, extra=vol.ALLOW_EXTRA) hass.services.async_register( diff --git a/homeassistant/components/homeassistant/services.yaml b/homeassistant/components/homeassistant/services.yaml index 251ee171b6a..da52ff50d2f 100644 --- a/homeassistant/components/homeassistant/services.yaml +++ b/homeassistant/components/homeassistant/services.yaml @@ -74,3 +74,9 @@ reload_config_entry: example: 8955375327824e14ba89e4b29cc3ec9a selector: text: + +save_persistent_states: + name: Save Persistent States + description: + Save the persistent states (for entities derived from RestoreEntity) immediately. + Maintain the normal periodic saving interval. diff --git a/homeassistant/const.py b/homeassistant/const.py index ccd42ca32bb..5f4f8cd084c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -612,6 +612,7 @@ SERVICE_CLOSE_COVER: Final = "close_cover" SERVICE_CLOSE_COVER_TILT: Final = "close_cover_tilt" SERVICE_OPEN_COVER: Final = "open_cover" SERVICE_OPEN_COVER_TILT: Final = "open_cover_tilt" +SERVICE_SAVE_PERSISTENT_STATES: Final = "save_persistent_states" SERVICE_SET_COVER_POSITION: Final = "set_cover_position" SERVICE_SET_COVER_TILT_POSITION: Final = "set_cover_tilt_position" SERVICE_STOP_COVER: Final = "stop_cover" diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 67b2d329af1..da4d2bacf15 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -100,6 +100,12 @@ class RestoreStateData: return cast(RestoreStateData, await load_instance(hass)) + @classmethod + async def async_save_persistent_states(cls, hass: HomeAssistant) -> None: + """Dump states now.""" + data = await cls.async_get_instance(hass) + await data.async_dump_states() + def __init__(self, hass: HomeAssistant) -> None: """Initialize the restore state data class.""" self.hass: HomeAssistant = hass diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index d12cc8d9a7b..fb4a0f4c1da 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -23,6 +23,7 @@ from homeassistant.const import ( EVENT_CORE_CONFIG_UPDATE, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, + SERVICE_SAVE_PERSISTENT_STATES, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -543,3 +544,18 @@ async def test_stop_homeassistant(hass): assert not mock_check.called await hass.async_block_till_done() assert mock_restart.called + + +async def test_save_persistent_states(hass): + """Test we can call save_persistent_states.""" + await async_setup_component(hass, "homeassistant", {}) + with patch( + "homeassistant.helpers.restore_state.RestoreStateData.async_save_persistent_states", + return_value=None, + ) as mock_save: + await hass.services.async_call( + "homeassistant", + SERVICE_SAVE_PERSISTENT_STATES, + blocking=True, + ) + assert mock_save.called diff --git a/tests/helpers/test_restore_state.py b/tests/helpers/test_restore_state.py index 1d3be2ca98d..d138a5381da 100644 --- a/tests/helpers/test_restore_state.py +++ b/tests/helpers/test_restore_state.py @@ -98,6 +98,62 @@ async def test_periodic_write(hass): assert not mock_write_data.called +async def test_save_persistent_states(hass): + """Test that we cancel the currently running job, save the data, and verify the perdiodic job continues.""" + data = await RestoreStateData.async_get_instance(hass) + await hass.async_block_till_done() + await data.store.async_save([]) + + # Emulate a fresh load + hass.data[DATA_RESTORE_STATE_TASK] = None + + entity = RestoreEntity() + entity.hass = hass + entity.entity_id = "input_boolean.b1" + + with patch( + "homeassistant.helpers.restore_state.Store.async_save" + ) as mock_write_data: + await entity.async_get_last_state() + await hass.async_block_till_done() + + # Startup Save + assert mock_write_data.called + + with patch( + "homeassistant.helpers.restore_state.Store.async_save" + ) as mock_write_data: + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) + await hass.async_block_till_done() + + # Not quite the first interval + assert not mock_write_data.called + + with patch( + "homeassistant.helpers.restore_state.Store.async_save" + ) as mock_write_data: + await RestoreStateData.async_save_persistent_states(hass) + await hass.async_block_till_done() + + assert mock_write_data.called + + with patch( + "homeassistant.helpers.restore_state.Store.async_save" + ) as mock_write_data: + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=20)) + await hass.async_block_till_done() + # Verify still saving + assert mock_write_data.called + + with patch( + "homeassistant.helpers.restore_state.Store.async_save" + ) as mock_write_data: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + # Verify normal shutdown + assert mock_write_data.called + + async def test_hass_starting(hass): """Test that we cache data.""" hass.state = CoreState.starting From 25f3cdde50dad6f5c536b02a42b8c3470475e7c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Aug 2021 18:03:22 -0500 Subject: [PATCH 095/355] Add powerwall import and export sensors (#54018) Co-authored-by: Bram Kragten --- homeassistant/components/powerwall/const.py | 2 - homeassistant/components/powerwall/sensor.py | 64 ++++++++++++++++++-- tests/components/powerwall/test_sensor.py | 23 ++++--- 3 files changed, 75 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/powerwall/const.py b/homeassistant/components/powerwall/const.py index c86333cb9f8..b2cd48df276 100644 --- a/homeassistant/components/powerwall/const.py +++ b/homeassistant/components/powerwall/const.py @@ -9,8 +9,6 @@ POWERWALL_API_CHANGED = "api_changed" UPDATE_INTERVAL = 30 ATTR_FREQUENCY = "frequency" -ATTR_ENERGY_EXPORTED = "energy_exported_(in_kW)" -ATTR_ENERGY_IMPORTED = "energy_imported_(in_kW)" ATTR_INSTANT_AVERAGE_VOLTAGE = "instant_average_voltage" ATTR_INSTANT_TOTAL_CURRENT = "instant_total_current" ATTR_IS_ACTIVE = "is_active" diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index d536c776bf0..b2281c515ae 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -6,14 +6,15 @@ from tesla_powerwall import MeterType from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ( DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, + ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_KILO_WATT, ) +import homeassistant.util.dt as dt_util from .const import ( - ATTR_ENERGY_EXPORTED, - ATTR_ENERGY_IMPORTED, ATTR_FREQUENCY, ATTR_INSTANT_AVERAGE_VOLTAGE, ATTR_INSTANT_TOTAL_CURRENT, @@ -29,6 +30,11 @@ from .const import ( ) from .entity import PowerWallEntity +_METER_DIRECTION_EXPORT = "export" +_METER_DIRECTION_IMPORT = "import" +_METER_DIRECTIONS = [_METER_DIRECTION_EXPORT, _METER_DIRECTION_IMPORT] + + _LOGGER = logging.getLogger(__name__) @@ -55,6 +61,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): powerwalls_serial_numbers, ) ) + for meter_direction in _METER_DIRECTIONS: + entities.append( + PowerWallEnergyDirectionSensor( + meter, + coordinator, + site_info, + status, + device_type, + powerwalls_serial_numbers, + meter_direction, + ) + ) entities.append( PowerWallChargeSensor( @@ -124,9 +142,47 @@ class PowerWallEnergySensor(PowerWallEntity, SensorEntity): meter = self.coordinator.data[POWERWALL_API_METERS].get_meter(self._meter) return { ATTR_FREQUENCY: round(meter.frequency, 1), - ATTR_ENERGY_EXPORTED: meter.get_energy_exported(), - ATTR_ENERGY_IMPORTED: meter.get_energy_imported(), ATTR_INSTANT_AVERAGE_VOLTAGE: round(meter.average_voltage, 1), ATTR_INSTANT_TOTAL_CURRENT: meter.get_instant_total_current(), ATTR_IS_ACTIVE: meter.is_active(), } + + +class PowerWallEnergyDirectionSensor(PowerWallEntity, SensorEntity): + """Representation of an Powerwall Direction Energy sensor.""" + + _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR + _attr_device_class = DEVICE_CLASS_ENERGY + _attr_last_reset = dt_util.utc_from_timestamp(0) + + def __init__( + self, + meter: MeterType, + coordinator, + site_info, + status, + device_type, + powerwalls_serial_numbers, + meter_direction, + ): + """Initialize the sensor.""" + super().__init__( + coordinator, site_info, status, device_type, powerwalls_serial_numbers + ) + self._meter = meter + self._meter_direction = meter_direction + self._attr_name = ( + f"Powerwall {self._meter.value.title()} {self._meter_direction.title()}" + ) + self._attr_unique_id = ( + f"{self.base_unique_id}_{self._meter.value}_{self._meter_direction}" + ) + + @property + def state(self): + """Get the current value in kWh.""" + meter = self.coordinator.data[POWERWALL_API_METERS].get_meter(self._meter) + if self._meter_direction == _METER_DIRECTION_EXPORT: + return meter.get_energy_exported() + return meter.get_energy_imported() diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index 32c7da9c78e..33c186e922c 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -39,8 +39,6 @@ async def test_sensors(hass): assert state.state == "0.032" expected_attributes = { "frequency": 60, - "energy_exported_(in_kW)": 10429.5, - "energy_imported_(in_kW)": 4824.2, "instant_average_voltage": 120.7, "unit_of_measurement": "kW", "friendly_name": "Powerwall Site Now", @@ -52,12 +50,16 @@ async def test_sensors(hass): for key, value in expected_attributes.items(): assert state.attributes[key] == value + assert float(hass.states.get("sensor.powerwall_site_export").state) == 10429.5 + assert float(hass.states.get("sensor.powerwall_site_import").state) == 4824.2 + + export_attributes = hass.states.get("sensor.powerwall_site_export").attributes + assert export_attributes["unit_of_measurement"] == "kWh" + state = hass.states.get("sensor.powerwall_load_now") assert state.state == "1.971" expected_attributes = { "frequency": 60, - "energy_exported_(in_kW)": 1056.8, - "energy_imported_(in_kW)": 4693.0, "instant_average_voltage": 120.7, "unit_of_measurement": "kW", "friendly_name": "Powerwall Load Now", @@ -69,12 +71,13 @@ async def test_sensors(hass): for key, value in expected_attributes.items(): assert state.attributes[key] == value + assert float(hass.states.get("sensor.powerwall_load_export").state) == 1056.8 + assert float(hass.states.get("sensor.powerwall_load_import").state) == 4693.0 + state = hass.states.get("sensor.powerwall_battery_now") assert state.state == "-8.55" expected_attributes = { "frequency": 60.0, - "energy_exported_(in_kW)": 3620.0, - "energy_imported_(in_kW)": 4216.2, "instant_average_voltage": 240.6, "unit_of_measurement": "kW", "friendly_name": "Powerwall Battery Now", @@ -86,12 +89,13 @@ async def test_sensors(hass): for key, value in expected_attributes.items(): assert state.attributes[key] == value + assert float(hass.states.get("sensor.powerwall_battery_export").state) == 3620.0 + assert float(hass.states.get("sensor.powerwall_battery_import").state) == 4216.2 + state = hass.states.get("sensor.powerwall_solar_now") assert state.state == "10.49" expected_attributes = { "frequency": 60, - "energy_exported_(in_kW)": 9864.2, - "energy_imported_(in_kW)": 28.2, "instant_average_voltage": 120.7, "unit_of_measurement": "kW", "friendly_name": "Powerwall Solar Now", @@ -103,6 +107,9 @@ async def test_sensors(hass): for key, value in expected_attributes.items(): assert state.attributes[key] == value + assert float(hass.states.get("sensor.powerwall_solar_export").state) == 9864.2 + assert float(hass.states.get("sensor.powerwall_solar_import").state) == 28.2 + state = hass.states.get("sensor.powerwall_charge") assert state.state == "47" expected_attributes = { From d80da944a315a2b84ad6c31aeda03c2980c912fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 10 Aug 2021 01:24:18 +0200 Subject: [PATCH 096/355] Version sensor entity cleanup (#53915) Co-authored-by: Franck Nijhof --- homeassistant/components/version/sensor.py | 117 ++++++++++----------- tests/components/version/test_sensor.py | 56 +++++++--- 2 files changed, 98 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index 04165ec9db1..f20f2682986 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -2,11 +2,19 @@ from datetime import timedelta import logging -from pyhaversion import HaVersion, HaVersionChannel, HaVersionSource -from pyhaversion.exceptions import HaVersionFetchException, HaVersionParseException +from pyhaversion import ( + HaVersion, + HaVersionChannel, + HaVersionSource, + exceptions as pyhaversionexceptions, +) import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import CONF_NAME, CONF_SOURCE from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -30,12 +38,10 @@ ALL_IMAGES = [ "raspberrypi4", "tinker", ] -ALL_SOURCES = [ - "container", - "haio", - "local", - "pypi", - "supervisor", + +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 ] @@ -48,8 +54,6 @@ DEFAULT_NAME_LATEST = "Latest Version" DEFAULT_NAME_LOCAL = "Current Version" DEFAULT_SOURCE = "local" -ICON = "mdi:package-up" - TIME_BETWEEN_UPDATES = timedelta(minutes=5) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -72,40 +76,42 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= name = config.get(CONF_NAME) source = config.get(CONF_SOURCE) + channel = HaVersionChannel.BETA if beta else HaVersionChannel.STABLE session = async_get_clientsession(hass) - channel = HaVersionChannel.BETA if beta else HaVersionChannel.STABLE + if source in HA_VERSION_SOURCES: + source = HaVersionSource(source) + elif source == "hassio": + source = HaVersionSource.SUPERVISOR + elif source == "docker": + source = HaVersionSource.CONTAINER - if source == "pypi": - haversion = VersionData( - HaVersion(session, source=HaVersionSource.PYPI, channel=channel) - ) - elif source in ["hassio", "supervisor"]: - haversion = VersionData( - HaVersion( - session, source=HaVersionSource.SUPERVISOR, channel=channel, image=image - ) - ) - elif source in ["docker", "container"]: - if image is not None and image != DEFAULT_IMAGE: - image = f"{image}-homeassistant" - haversion = VersionData( - HaVersion( - session, source=HaVersionSource.CONTAINER, channel=channel, image=image - ) - ) - elif source == "haio": - haversion = VersionData(HaVersion(session, source=HaVersionSource.HAIO)) - else: - haversion = VersionData(HaVersion(session, source=HaVersionSource.LOCAL)) + if ( + source in (HaVersionSource.SUPERVISOR, HaVersionSource.CONTAINER) + and image is not None + and image != DEFAULT_IMAGE + ): + image = f"{image}-homeassistant" - if not name: - if source == DEFAULT_SOURCE: + if not (name := config.get(CONF_NAME)): + if source == HaVersionSource.LOCAL: name = DEFAULT_NAME_LOCAL else: name = DEFAULT_NAME_LATEST - async_add_entities([VersionSensor(haversion, name)], True) + async_add_entities( + [ + VersionSensor( + VersionData( + HaVersion( + session=session, source=source, image=image, channel=channel + ) + ), + SensorEntityDescription(key=source, name=name), + ) + ], + True, + ) class VersionData: @@ -120,9 +126,9 @@ class VersionData: """Get the latest version information.""" try: await self.api.get_version() - except HaVersionFetchException as exception: + except pyhaversionexceptions.HaVersionFetchException as exception: _LOGGER.warning(exception) - except HaVersionParseException as exception: + except pyhaversionexceptions.HaVersionParseException as exception: _LOGGER.warning( "Could not parse data received for %s - %s", self.api.source, exception ) @@ -131,32 +137,19 @@ class VersionData: class VersionSensor(SensorEntity): """Representation of a Home Assistant version sensor.""" - def __init__(self, data: VersionData, name: str) -> None: + _attr_icon = "mdi:package-up" + + def __init__( + self, + data: VersionData, + description: SensorEntityDescription, + ) -> None: """Initialize the Version sensor.""" self.data = data - self._name = name - self._state = None + self.entity_description = description async def async_update(self): """Get the latest version information.""" await self.data.async_update() - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self.data.api.version - - @property - def extra_state_attributes(self): - """Return attributes for the sensor.""" - return self.data.api.version_data - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return ICON + self._attr_state = self.data.api.version + self._attr_extra_state_attributes = self.data.api.version_data diff --git a/tests/components/version/test_sensor.py b/tests/components/version/test_sensor.py index 164b4090e5f..1f64fe23039 100644 --- a/tests/components/version/test_sensor.py +++ b/tests/components/version/test_sensor.py @@ -1,26 +1,56 @@ """The test for the version sensor platform.""" from unittest.mock import patch +from pyhaversion import HaVersionSource, exceptions as pyhaversionexceptions +import pytest + +from homeassistant.components.version.sensor import ALL_SOURCES from homeassistant.setup import async_setup_component MOCK_VERSION = "10.0" -async def test_version_sensor(hass): - """Test the Version sensor.""" - config = {"sensor": {"platform": "version"}} +@pytest.mark.parametrize( + "source", + ALL_SOURCES, +) +async def test_version_source(hass, source): + """Test the Version sensor with different sources.""" + config = { + "sensor": {"platform": "version", "source": source, "image": "qemux86-64"} + } - assert await async_setup_component(hass, "sensor", config) - - -async def test_version(hass): - """Test the Version sensor.""" - config = {"sensor": {"platform": "version", "name": "test"}} - - with patch("homeassistant.const.__version__", MOCK_VERSION): + with patch("pyhaversion.version.HaVersion.version", MOCK_VERSION): assert await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() - state = hass.states.get("sensor.test") + name = "current_version" if source == HaVersionSource.LOCAL else "latest_version" + state = hass.states.get(f"sensor.{name}") - assert state.state == "10.0" + 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( + "pyhaversion.version.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( + "pyhaversion.version.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 From a40deac714c6531bac7557f679502b3db209617a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 9 Aug 2021 16:45:39 -0700 Subject: [PATCH 097/355] Revert "Use entity class attributes for Bluesound (#53033)" (#54365) --- .../components/bluesound/media_player.py | 185 +++++++++++------- 1 file changed, 115 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index a565a0f560c..86d0be72bdc 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -203,29 +203,33 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class BluesoundPlayer(MediaPlayerEntity): """Representation of a Bluesound Player.""" - _attr_media_content_type = MEDIA_TYPE_MUSIC - - def __init__(self, hass, host, port=DEFAULT_PORT, name=None, init_callback=None): + def __init__(self, hass, host, port=None, name=None, init_callback=None): """Initialize the media player.""" self.host = host self._hass = hass self.port = port self._polling_session = async_get_clientsession(hass) self._polling_task = None # The actual polling task. - self._attr_name = name + self._name = name + self._icon = None self._capture_items = [] self._services_items = [] self._preset_items = [] self._sync_status = {} self._status = None - self._is_online = None + self._last_status_update = None + self._is_online = False self._retry_remove = None + self._muted = False self._master = None - self._group_name = None - self._bluesound_device_name = None self._is_master = False + self._group_name = None self._group_list = [] + self._bluesound_device_name = None + self._init_callback = init_callback + if self.port is None: + self.port = DEFAULT_PORT class _TimeoutException(Exception): pass @@ -248,12 +252,12 @@ class BluesoundPlayer(MediaPlayerEntity): return None self._sync_status = resp["SyncStatus"].copy() - if not self.name: - self._attr_name = self._sync_status.get("@name", self.host) + if not self._name: + self._name = self._sync_status.get("@name", self.host) if not self._bluesound_device_name: self._bluesound_device_name = self._sync_status.get("@name", self.host) - if not self.icon: - self._attr_icon = self._sync_status.get("@icon", self.host) + if not self._icon: + self._icon = self._sync_status.get("@icon", self.host) master = self._sync_status.get("master") if master is not None: @@ -287,14 +291,14 @@ class BluesoundPlayer(MediaPlayerEntity): await self.async_update_status() except (asyncio.TimeoutError, ClientError, BluesoundPlayer._TimeoutException): - _LOGGER.info("Node %s is offline, retrying later", self.name) + _LOGGER.info("Node %s is offline, retrying later", self._name) await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT) self.start_polling() except CancelledError: - _LOGGER.debug("Stopping the polling of node %s", self.name) + _LOGGER.debug("Stopping the polling of node %s", self._name) except Exception: - _LOGGER.exception("Unexpected error in %s", self.name) + _LOGGER.exception("Unexpected error in %s", self._name) raise def start_polling(self): @@ -398,7 +402,7 @@ class BluesoundPlayer(MediaPlayerEntity): if response.status == HTTP_OK: result = await response.text() self._is_online = True - self._attr_media_position_updated_at = dt_util.utcnow() + self._last_status_update = dt_util.utcnow() self._status = xmltodict.parse(result)["status"].copy() group_name = self._status.get("groupName") @@ -434,58 +438,11 @@ class BluesoundPlayer(MediaPlayerEntity): except (asyncio.TimeoutError, ClientError): self._is_online = False - self._attr_media_position_updated_at = None + self._last_status_update = None self._status = None self.async_write_ha_state() - _LOGGER.info("Client connection error, marking %s as offline", self.name) + _LOGGER.info("Client connection error, marking %s as offline", self._name) raise - self.update_state_attr() - - def update_state_attr(self): - """Update state attributes.""" - if self._status is None: - self._attr_state = STATE_OFF - self._attr_supported_features = 0 - elif self.is_grouped and not self.is_master: - self._attr_state = STATE_GROUPED - self._attr_supported_features = ( - SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE - ) - else: - status = self._status.get("state") - self._attr_state = STATE_IDLE - if status in ("pause", "stop"): - self._attr_state = STATE_PAUSED - elif status in ("stream", "play"): - self._attr_state = STATE_PLAYING - supported = SUPPORT_CLEAR_PLAYLIST - if self._status.get("indexing", "0") == "0": - supported = ( - supported - | SUPPORT_PAUSE - | SUPPORT_PREVIOUS_TRACK - | SUPPORT_NEXT_TRACK - | SUPPORT_PLAY_MEDIA - | SUPPORT_STOP - | SUPPORT_PLAY - | SUPPORT_SELECT_SOURCE - | SUPPORT_SHUFFLE_SET - ) - if self.volume_level is not None and self.volume_level >= 0: - supported = ( - supported - | SUPPORT_VOLUME_STEP - | SUPPORT_VOLUME_SET - | SUPPORT_VOLUME_MUTE - ) - if self._status.get("canSeek", "") == "1": - supported = supported | SUPPORT_SEEK - self._attr_supported_features = supported - self._attr_extra_state_attributes = {} - if self._group_list: - self._attr_extra_state_attributes = {ATTR_BLUESOUND_GROUP: self._group_list} - self._attr_extra_state_attributes[ATTR_MASTER] = self._is_master - self._attr_shuffle = self._status.get("shuffle", "0") == "1" async def async_trigger_sync_on_all(self): """Trigger sync status update on all devices.""" @@ -585,6 +542,27 @@ class BluesoundPlayer(MediaPlayerEntity): return self._services_items + @property + def media_content_type(self): + """Content type of current playing media.""" + return MEDIA_TYPE_MUSIC + + @property + def state(self): + """Return the state of the device.""" + if self._status is None: + return STATE_OFF + + if self.is_grouped and not self.is_master: + return STATE_GROUPED + + status = self._status.get("state") + if status in ("pause", "stop"): + return STATE_PAUSED + if status in ("stream", "play"): + return STATE_PLAYING + return STATE_IDLE + @property def media_title(self): """Title of current playing media.""" @@ -639,7 +617,7 @@ class BluesoundPlayer(MediaPlayerEntity): return None mediastate = self.state - if self.media_position_updated_at is None or mediastate == STATE_IDLE: + if self._last_status_update is None or mediastate == STATE_IDLE: return None position = self._status.get("secs") @@ -648,9 +626,7 @@ class BluesoundPlayer(MediaPlayerEntity): position = float(position) if mediastate == STATE_PLAYING: - position += ( - dt_util.utcnow() - self.media_position_updated_at - ).total_seconds() + position += (dt_util.utcnow() - self._last_status_update).total_seconds() return position @@ -665,6 +641,11 @@ class BluesoundPlayer(MediaPlayerEntity): return None return float(duration) + @property + def media_position_updated_at(self): + """Last time status was updated.""" + return self._last_status_update + @property def volume_level(self): """Volume level of the media player (0..1).""" @@ -687,11 +668,21 @@ class BluesoundPlayer(MediaPlayerEntity): mute = bool(int(mute)) return mute + @property + def name(self): + """Return the name of the device.""" + return self._name + @property def bluesound_device_name(self): """Return the device name as returned by the device.""" return self._bluesound_device_name + @property + def icon(self): + """Return the icon of the device.""" + return self._icon + @property def source_list(self): """List of available input sources.""" @@ -787,15 +778,58 @@ class BluesoundPlayer(MediaPlayerEntity): return None @property - def is_master(self) -> bool: + def supported_features(self): + """Flag of media commands that are supported.""" + if self._status is None: + return 0 + + if self.is_grouped and not self.is_master: + return SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE + + supported = SUPPORT_CLEAR_PLAYLIST + + if self._status.get("indexing", "0") == "0": + supported = ( + supported + | SUPPORT_PAUSE + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_PLAY_MEDIA + | SUPPORT_STOP + | SUPPORT_PLAY + | SUPPORT_SELECT_SOURCE + | SUPPORT_SHUFFLE_SET + ) + + current_vol = self.volume_level + if current_vol is not None and current_vol >= 0: + supported = ( + supported + | SUPPORT_VOLUME_STEP + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_MUTE + ) + + if self._status.get("canSeek", "") == "1": + supported = supported | SUPPORT_SEEK + + return supported + + @property + def is_master(self): """Return true if player is a coordinator.""" return self._is_master @property - def is_grouped(self) -> bool: + def is_grouped(self): """Return true if player is a coordinator.""" return self._master is not None or self._is_master + @property + def shuffle(self): + """Return true if shuffle is active.""" + return self._status.get("shuffle", "0") == "1" + async def async_join(self, master): """Join the player to a group.""" master_device = [ @@ -815,6 +849,17 @@ class BluesoundPlayer(MediaPlayerEntity): else: _LOGGER.error("Master not found %s", master_device) + @property + def extra_state_attributes(self): + """List members in group.""" + attributes = {} + if self._group_list: + attributes = {ATTR_BLUESOUND_GROUP: self._group_list} + + attributes[ATTR_MASTER] = self._is_master + + return attributes + def rebuild_bluesound_group(self): """Rebuild the list of entities in speaker group.""" if self._group_name is None: From 38a7bdbcf35327de54461daf59778bc0c665a207 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 9 Aug 2021 16:45:56 -0700 Subject: [PATCH 098/355] Do not process forwarded for headers for cloud requests (#54364) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/components/http/forwarded.py | 11 ++++++++++- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/http/test_forwarded.py | 20 ++++++++++++++++++++ 6 files changed, 34 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 7516f32c3e1..abf73c1d54b 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.44.0"], + "requirements": ["hass-nabucasa==0.45.1"], "dependencies": ["http", "webhook"], "after_dependencies": ["google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"], diff --git a/homeassistant/components/http/forwarded.py b/homeassistant/components/http/forwarded.py index 18bc51af1d1..9a76866ba21 100644 --- a/homeassistant/components/http/forwarded.py +++ b/homeassistant/components/http/forwarded.py @@ -63,12 +63,19 @@ def async_setup_forwarded( an HTTP 400 status code is thrown. """ + try: + from hass_nabucasa import remote # pylint: disable=import-outside-toplevel + except ImportError: + remote = None + @middleware async def forwarded_middleware( request: Request, handler: Callable[[Request], Awaitable[StreamResponse]] ) -> StreamResponse: """Process forwarded data by a reverse proxy.""" - overrides: dict[str, str] = {} + # Skip requests from Remote UI + if remote is not None and remote.is_cloud_request.get(): + return await handler(request) # Handle X-Forwarded-For forwarded_for_headers: list[str] = request.headers.getall(X_FORWARDED_FOR, []) @@ -120,6 +127,8 @@ def async_setup_forwarded( ) raise HTTPBadRequest from err + overrides: dict[str, str] = {} + # Find the last trusted index in the X-Forwarded-For list forwarded_for_index = 0 for forwarded_ip in forwarded_for: diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2e07d0adc18..d03c4b7c4f7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ cryptography==3.3.2 defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 -hass-nabucasa==0.44.0 +hass-nabucasa==0.45.1 home-assistant-frontend==20210809.0 httpx==0.18.2 ifaddr==0.1.7 diff --git a/requirements_all.txt b/requirements_all.txt index b71b7fcc2c0..397594de771 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -753,7 +753,7 @@ habitipy==0.2.0 hangups==0.4.14 # homeassistant.components.cloud -hass-nabucasa==0.44.0 +hass-nabucasa==0.45.1 # homeassistant.components.splunk hass_splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 774721010f8..5004aa26dd7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -431,7 +431,7 @@ habitipy==0.2.0 hangups==0.4.14 # homeassistant.components.cloud -hass-nabucasa==0.44.0 +hass-nabucasa==0.45.1 # homeassistant.components.tasmota hatasmota==0.2.20 diff --git a/tests/components/http/test_forwarded.py b/tests/components/http/test_forwarded.py index 400a1f32729..42e67416044 100644 --- a/tests/components/http/test_forwarded.py +++ b/tests/components/http/test_forwarded.py @@ -1,5 +1,6 @@ """Test real forwarded middleware.""" from ipaddress import ip_network +from unittest.mock import Mock, patch from aiohttp import web from aiohttp.hdrs import X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO @@ -441,3 +442,22 @@ async def test_x_forwarded_host_with_empty_header(aiohttp_client, caplog): assert resp.status == 400 assert "Empty value received in X-Forward-Host header" in caplog.text + + +async def test_x_forwarded_cloud(aiohttp_client, caplog): + """Test that cloud requests are not processed.""" + app = web.Application() + app.router.add_get("/", mock_handler) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) + + mock_api_client = await aiohttp_client(app) + + with patch( + "hass_nabucasa.remote.is_cloud_request", Mock(get=Mock(return_value=True)) + ): + resp = await mock_api_client.get( + "/", headers={X_FORWARDED_FOR: "222.222.222.222", X_FORWARDED_HOST: ""} + ) + + # This request would normally fail because it's invalid, now it works. + assert resp.status == 200 From 1948d11d84ae3f758571881eb9aa6245e24df7fe Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Tue, 10 Aug 2021 02:10:53 +0200 Subject: [PATCH 099/355] AsusWRT remove default EntityDescription property (#54367) --- homeassistant/components/asuswrt/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 6c0671b53cb..ef186a80085 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -49,7 +49,6 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( name="Devices Connected", icon="mdi:router-network", unit_of_measurement=UNIT_DEVICES, - entity_registry_enabled_default=True, ), AsusWrtSensorEntityDescription( key=SENSORS_RATES[0], From f92f0bb87bc6686c2686c57b11669f1c0c105656 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 10 Aug 2021 04:25:22 +0200 Subject: [PATCH 100/355] Use EntityDescription - juicenet (#54362) * Use EntityDescription - juicenet * Move part of icon to EntityDescription * Remove default values * Remove name override to use the _attr_name Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- homeassistant/components/juicenet/entity.py | 5 - homeassistant/components/juicenet/sensor.py | 137 +++++++++++--------- 2 files changed, 77 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/juicenet/entity.py b/homeassistant/components/juicenet/entity.py index 759979c5f11..9b1def3b678 100644 --- a/homeassistant/components/juicenet/entity.py +++ b/homeassistant/components/juicenet/entity.py @@ -14,11 +14,6 @@ class JuiceNetDevice(CoordinatorEntity): self.device = device self.type = sensor_type - @property - def name(self): - """Return the name of the device.""" - return self.device.name - @property def unique_id(self): """Return a unique ID.""" diff --git a/homeassistant/components/juicenet/sensor.py b/homeassistant/components/juicenet/sensor.py index 51792daf38c..2b8bd61e1fb 100644 --- a/homeassistant/components/juicenet/sensor.py +++ b/homeassistant/components/juicenet/sensor.py @@ -1,5 +1,11 @@ """Support for monitoring juicenet/juicepoint/juicebox based EVSE sensors.""" -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from __future__ import annotations + +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, @@ -17,61 +23,86 @@ from homeassistant.const import ( from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR from .entity import JuiceNetDevice -SENSOR_TYPES = { - "status": ["Charging Status", None, None, None], - "temperature": [ - "Temperature", - TEMP_CELSIUS, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, - ], - "voltage": ["Voltage", ELECTRIC_POTENTIAL_VOLT, DEVICE_CLASS_VOLTAGE, None], - "amps": [ - "Amps", - ELECTRIC_CURRENT_AMPERE, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, - ], - "watts": ["Watts", POWER_WATT, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT], - "charge_time": ["Charge time", TIME_SECONDS, None, None], - "energy_added": ["Energy added", ENERGY_WATT_HOUR, DEVICE_CLASS_ENERGY, None], -} +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="status", + name="Charging Status", + ), + SensorEntityDescription( + key="temperature", + name="Temperature", + unit_of_measurement=TEMP_CELSIUS, + icon="mdi:thermometer", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="voltage", + name="Voltage", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + icon="mdi:flash", + device_class=DEVICE_CLASS_VOLTAGE, + ), + SensorEntityDescription( + key="amps", + name="Amps", + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + icon="mdi:flash", + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="watts", + name="Watts", + unit_of_measurement=POWER_WATT, + icon="mdi:flash", + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="charge_time", + name="Charge time", + unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", + ), + SensorEntityDescription( + key="energy_added", + name="Energy added", + unit_of_measurement=ENERGY_WATT_HOUR, + icon="mdi:flash", + device_class=DEVICE_CLASS_ENERGY, + ), +) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the JuiceNet Sensors.""" - entities = [] juicenet_data = hass.data[DOMAIN][config_entry.entry_id] api = juicenet_data[JUICENET_API] coordinator = juicenet_data[JUICENET_COORDINATOR] - for device in api.devices: - for sensor in SENSOR_TYPES: - entities.append(JuiceNetSensorDevice(device, sensor, coordinator)) + entities = [ + JuiceNetSensorDevice(device, coordinator, description) + for device in api.devices + for description in SENSOR_TYPES + ] async_add_entities(entities) class JuiceNetSensorDevice(JuiceNetDevice, SensorEntity): """Implementation of a JuiceNet sensor.""" - def __init__(self, device, sensor_type, coordinator): + def __init__(self, device, coordinator, description: SensorEntityDescription): """Initialise the sensor.""" - super().__init__(device, sensor_type, coordinator) - self._name = SENSOR_TYPES[sensor_type][0] - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] - self._attr_device_class = SENSOR_TYPES[sensor_type][2] - self._attr_state_class = SENSOR_TYPES[sensor_type][3] - - @property - def name(self): - """Return the name of the device.""" - return f"{self.device.name} {self._name}" + super().__init__(device, description.key, coordinator) + self.entity_description = description + self._attr_name = f"{self.device.name} {description.name}" @property def icon(self): """Return the icon of the sensor.""" icon = None - if self.type == "status": + if self.entity_description.key == "status": status = self.device.status if status == "standby": icon = "mdi:power-plug-off" @@ -79,42 +110,28 @@ class JuiceNetSensorDevice(JuiceNetDevice, SensorEntity): icon = "mdi:power-plug" elif status == "charging": icon = "mdi:battery-positive" - elif self.type == "temperature": - icon = "mdi:thermometer" - elif self.type == "voltage": - icon = "mdi:flash" - elif self.type == "amps": - icon = "mdi:flash" - elif self.type == "watts": - icon = "mdi:flash" - elif self.type == "charge_time": - icon = "mdi:timer-outline" - elif self.type == "energy_added": - icon = "mdi:flash" + else: + icon = self.entity_description.icon return icon - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit_of_measurement - @property def state(self): """Return the state.""" state = None - if self.type == "status": + sensor_type = self.entity_description.key + if sensor_type == "status": state = self.device.status - elif self.type == "temperature": + elif sensor_type == "temperature": state = self.device.temperature - elif self.type == "voltage": + elif sensor_type == "voltage": state = self.device.voltage - elif self.type == "amps": + elif sensor_type == "amps": state = self.device.amps - elif self.type == "watts": + elif sensor_type == "watts": state = self.device.watts - elif self.type == "charge_time": + elif sensor_type == "charge_time": state = self.device.charge_time - elif self.type == "energy_added": + elif sensor_type == "energy_added": state = self.device.energy_added else: state = "Unknown" From f60fbf719773245c80ac2214912e54572715b73b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 9 Aug 2021 23:16:18 -0400 Subject: [PATCH 101/355] Update Climacell rate limit (#54373) --- homeassistant/components/climacell/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index 9e80c769abf..162fbb01545 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -66,7 +66,7 @@ DEFAULT_FORECAST_TYPE = DAILY DOMAIN = "climacell" ATTRIBUTION = "Powered by ClimaCell" -MAX_REQUESTS_PER_DAY = 500 +MAX_REQUESTS_PER_DAY = 100 CLEAR_CONDITIONS = {"night": ATTR_CONDITION_CLEAR_NIGHT, "day": ATTR_CONDITION_SUNNY} From b76899f546975f75fa56cdde57e344560ae73025 Mon Sep 17 00:00:00 2001 From: Brett Date: Tue, 10 Aug 2021 13:21:24 +1000 Subject: [PATCH 102/355] Fix race condition in Advantage Air (#53439) --- .../components/advantage_air/climate.py | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index 1d377abc065..1e6027b8db6 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -15,7 +15,6 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS -from homeassistant.core import callback from homeassistant.helpers import entity_platform from .const import ( @@ -166,19 +165,22 @@ class AdvantageAirZone(AdvantageAirClimateEntity): f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}' ) - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove(self.coordinator.async_add_listener(self._update_callback)) - - @callback - def _update_callback(self) -> None: - """Load data from integration.""" - self._attr_current_temperature = self._zone["measuredTemp"] - self._attr_target_temperature = self._zone["setTemp"] - self._attr_hvac_mode = HVAC_MODE_OFF + @property + def hvac_mode(self): + """Return the current state as HVAC mode.""" if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN: - self._attr_hvac_mode = HVAC_MODE_FAN_ONLY - self.async_write_ha_state() + return HVAC_MODE_FAN_ONLY + return HVAC_MODE_OFF + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._zone["measuredTemp"] + + @property + def target_temperature(self): + """Return the target temperature.""" + return self._zone["setTemp"] async def async_set_hvac_mode(self, hvac_mode): """Set the HVAC Mode and State.""" From 3d31bd5c684b6eb4495ac97c0e7db8fb19f00080 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 10 Aug 2021 05:56:04 +0200 Subject: [PATCH 103/355] Upgrade codecov to 2.1.12 (#54370) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index aceec3229a9..acfe29db593 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -4,7 +4,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt -codecov==2.1.11 +codecov==2.1.12 coverage==5.5 jsonpickle==1.4.1 mock-open==1.4.0 From 3202d4882a2ea043b8bdfa16c643a31ad3c224a3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 10 Aug 2021 05:56:19 +0200 Subject: [PATCH 104/355] Upgrade debugpy to 1.4.1 (#54369) --- homeassistant/components/debugpy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json index a67d7181a90..3ff5d087e14 100644 --- a/homeassistant/components/debugpy/manifest.json +++ b/homeassistant/components/debugpy/manifest.json @@ -2,7 +2,7 @@ "domain": "debugpy", "name": "Remote Python Debugger", "documentation": "https://www.home-assistant.io/integrations/debugpy", - "requirements": ["debugpy==1.4.0"], + "requirements": ["debugpy==1.4.1"], "codeowners": ["@frenck"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 397594de771..6be000417be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -486,7 +486,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.4.0 +debugpy==1.4.1 # homeassistant.components.decora # decora==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5004aa26dd7..69e3bd85d32 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -282,7 +282,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.4.0 +debugpy==1.4.1 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 934662cd54610a024bf9da9f7929e43fb18e8980 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 9 Aug 2021 23:17:47 -0700 Subject: [PATCH 105/355] Handle CO2Signal response value being None (#54377) --- homeassistant/components/co2signal/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index bd8d94355fd..ea1cd1f6169 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -118,7 +118,8 @@ class CO2Sensor(update_coordinator.CoordinatorEntity[CO2SignalResponse], SensorE def available(self) -> bool: """Return True if entity is available.""" return ( - super().available and self._description.key in self.coordinator.data["data"] + super().available + and self.coordinator.data["data"].get(self._description.key) is not None ) @property From 8cb3a485e07374c538c593aa94388549dd149e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 10 Aug 2021 09:19:28 +0200 Subject: [PATCH 106/355] Fix Canary sensor state (#54380) --- homeassistant/components/canary/sensor.py | 6 +++++- tests/components/canary/test_sensor.py | 21 ++++++++++++--------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 5c92f0089f2..acb885055a3 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -113,7 +113,6 @@ class CanarySensor(CoordinatorEntity, SensorEntity): canary_sensor_type = SensorType.BATTERY self._canary_type = canary_sensor_type - self._attr_state = self.reading self._attr_unique_id = f"{device.device_id}_{sensor_type[0]}" self._attr_device_info = { "identifiers": {(DOMAIN, str(device.device_id))}, @@ -144,6 +143,11 @@ class CanarySensor(CoordinatorEntity, SensorEntity): return None + @property + def state(self) -> float | None: + """Return the state of the sensor.""" + return self.reading + @property def extra_state_attributes(self) -> dict[str, str] | None: """Return the state attributes.""" diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index 6419f81a62e..67d4a724584 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -118,9 +118,10 @@ async def test_sensors_attributes_pro(hass, canary) -> None: await hass.async_block_till_done() entity_id = "sensor.home_dining_room_air_quality" - state = hass.states.get(entity_id) - assert state - assert state.attributes[ATTR_AIR_QUALITY] == STATE_AIR_QUALITY_ABNORMAL + state1 = hass.states.get(entity_id) + assert state1 + assert state1.state == "0.59" + assert state1.attributes[ATTR_AIR_QUALITY] == STATE_AIR_QUALITY_ABNORMAL instance.get_latest_readings.return_value = [ mock_reading("temperature", "21.12"), @@ -133,9 +134,10 @@ async def test_sensors_attributes_pro(hass, canary) -> None: await hass.helpers.entity_component.async_update_entity(entity_id) await hass.async_block_till_done() - state = hass.states.get(entity_id) - assert state - assert state.attributes[ATTR_AIR_QUALITY] == STATE_AIR_QUALITY_VERY_ABNORMAL + state2 = hass.states.get(entity_id) + assert state2 + assert state2.state == "0.4" + assert state2.attributes[ATTR_AIR_QUALITY] == STATE_AIR_QUALITY_VERY_ABNORMAL instance.get_latest_readings.return_value = [ mock_reading("temperature", "21.12"), @@ -148,9 +150,10 @@ async def test_sensors_attributes_pro(hass, canary) -> None: await hass.helpers.entity_component.async_update_entity(entity_id) await hass.async_block_till_done() - state = hass.states.get(entity_id) - assert state - assert state.attributes[ATTR_AIR_QUALITY] == STATE_AIR_QUALITY_NORMAL + state3 = hass.states.get(entity_id) + assert state3 + assert state3.state == "1.0" + assert state3.attributes[ATTR_AIR_QUALITY] == STATE_AIR_QUALITY_NORMAL async def test_sensors_flex(hass, canary) -> None: From 54538bb72bc589e2a415d5e5f0dee4c466e438c0 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 10 Aug 2021 10:16:38 +0200 Subject: [PATCH 107/355] Bump pymodbus version to 2.5.3rc1 (#54318) --- homeassistant/components/modbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 9f2208de175..549ad2c2351 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -2,7 +2,7 @@ "domain": "modbus", "name": "Modbus", "documentation": "https://www.home-assistant.io/integrations/modbus", - "requirements": ["pymodbus==2.5.2"], + "requirements": ["pymodbus==2.5.3rc1"], "codeowners": ["@adamchengtkc", "@janiversen", "@vzahradnik"], "quality_scale": "silver", "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 6be000417be..7c4001c4c5a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1601,7 +1601,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==2.5.2 +pymodbus==2.5.3rc1 # homeassistant.components.monoprice pymonoprice==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69e3bd85d32..4f0b541ad31 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -914,7 +914,7 @@ pymfy==0.11.0 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==2.5.2 +pymodbus==2.5.3rc1 # homeassistant.components.monoprice pymonoprice==0.3 From fc1babfc92ec975888b62a3509263e31a0e23f46 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 10 Aug 2021 10:45:56 +0200 Subject: [PATCH 108/355] Activate mypy for Filter (#54044) --- homeassistant/components/filter/sensor.py | 6 +++--- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 97412823b30..c40c703b846 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -405,7 +405,7 @@ class Filter: :param entity: used for debugging only """ if isinstance(window_size, int): - self.states = deque(maxlen=window_size) + self.states: deque = deque(maxlen=window_size) self.window_unit = WINDOW_SIZE_UNIT_NUMBER_EVENTS else: self.states = deque(maxlen=0) @@ -476,7 +476,7 @@ class RangeFilter(Filter, SensorEntity): super().__init__(FILTER_NAME_RANGE, precision=precision, entity=entity) self._lower_bound = lower_bound self._upper_bound = upper_bound - self._stats_internal = Counter() + self._stats_internal: Counter = Counter() def _filter_state(self, new_state): """Implement the range filter.""" @@ -522,7 +522,7 @@ class OutlierFilter(Filter, SensorEntity): """ super().__init__(FILTER_NAME_OUTLIER, window_size, precision, entity) self._radius = radius - self._stats_internal = Counter() + self._stats_internal: Counter = Counter() self._store_raw = True def _filter_state(self, new_state): diff --git a/mypy.ini b/mypy.ini index 9c54f7a043f..4ad0dc9235f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1349,9 +1349,6 @@ ignore_errors = true [mypy-homeassistant.components.evohome.*] ignore_errors = true -[mypy-homeassistant.components.filter.*] -ignore_errors = true - [mypy-homeassistant.components.fireservicerota.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index e3b76747be2..38409ef8457 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -42,7 +42,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.enphase_envoy.*", "homeassistant.components.entur_public_transport.*", "homeassistant.components.evohome.*", - "homeassistant.components.filter.*", "homeassistant.components.fireservicerota.*", "homeassistant.components.firmata.*", "homeassistant.components.flo.*", From 020759d01d86507ee0d66859ba03218905f402bc Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 10 Aug 2021 10:46:33 +0200 Subject: [PATCH 109/355] Activate mypy for Alexa (#54042) --- homeassistant/components/alexa/capabilities.py | 2 +- homeassistant/components/alexa/errors.py | 6 ++++-- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index db1fa990c54..fcd6ebf6ae2 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -99,7 +99,7 @@ class AlexaCapability: return False @staticmethod - def properties_non_controllable() -> bool: + def properties_non_controllable() -> bool | None: """Return True if non controllable.""" return None diff --git a/homeassistant/components/alexa/errors.py b/homeassistant/components/alexa/errors.py index 29643bacc53..a6adc488f75 100644 --- a/homeassistant/components/alexa/errors.py +++ b/homeassistant/components/alexa/errors.py @@ -1,4 +1,6 @@ """Alexa related errors.""" +from __future__ import annotations + from homeassistant.exceptions import HomeAssistantError from .const import API_TEMP_UNITS @@ -22,8 +24,8 @@ class AlexaError(Exception): A handler can raise subclasses of this to return an error to the request. """ - namespace = None - error_type = None + namespace: str | None = None + error_type: str | None = None def __init__(self, error_message, payload=None): """Initialize an alexa error.""" diff --git a/mypy.ini b/mypy.ini index 4ad0dc9235f..d7b01ea939b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1271,9 +1271,6 @@ ignore_errors = true [mypy-homeassistant.components.aemet.*] ignore_errors = true -[mypy-homeassistant.components.alexa.*] -ignore_errors = true - [mypy-homeassistant.components.almond.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 38409ef8457..4ed672f8e01 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -16,7 +16,6 @@ from .model import Config, Integration IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.adguard.*", "homeassistant.components.aemet.*", - "homeassistant.components.alexa.*", "homeassistant.components.almond.*", "homeassistant.components.amcrest.*", "homeassistant.components.analytics.*", From 7e2c6ae332b6f3679bb3b20e3960763040d8aed0 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 10 Aug 2021 10:47:17 +0200 Subject: [PATCH 110/355] Activate mypy for Pilight (#53956) --- homeassistant/components/pilight/__init__.py | 2 +- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/pilight/__init__.py b/homeassistant/components/pilight/__init__.py index 02d56c890fe..5dbad2838bc 100644 --- a/homeassistant/components/pilight/__init__.py +++ b/homeassistant/components/pilight/__init__.py @@ -136,7 +136,7 @@ class CallRateDelayThrottle: def __init__(self, hass, delay_seconds: float) -> None: """Initialize the delay handler.""" self._delay = timedelta(seconds=max(0.0, delay_seconds)) - self._queue = [] + self._queue: list = [] self._active = False self._lock = threading.Lock() self._next_ts = dt_util.utcnow() diff --git a/mypy.ini b/mypy.ini index d7b01ea939b..22cd0c478d5 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1574,9 +1574,6 @@ ignore_errors = true [mypy-homeassistant.components.philips_js.*] ignore_errors = true -[mypy-homeassistant.components.pilight.*] -ignore_errors = true - [mypy-homeassistant.components.ping.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 4ed672f8e01..31e364d6062 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -117,7 +117,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.ozw.*", "homeassistant.components.panasonic_viera.*", "homeassistant.components.philips_js.*", - "homeassistant.components.pilight.*", "homeassistant.components.ping.*", "homeassistant.components.pioneer.*", "homeassistant.components.plaato.*", From d8c679809faebcdcfa5bf200c04ad2b797c9e07c Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 10 Aug 2021 10:47:57 +0200 Subject: [PATCH 111/355] Activate mypy for SiteSage Emonitor (#54040) --- homeassistant/components/emonitor/__init__.py | 2 +- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/emonitor/__init__.py b/homeassistant/components/emonitor/__init__.py index 69c8b907b72..91263db5127 100644 --- a/homeassistant/components/emonitor/__init__.py +++ b/homeassistant/components/emonitor/__init__.py @@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session = aiohttp_client.async_get_clientsession(hass) emonitor = Emonitor(entry.data[CONF_HOST], session) - coordinator = DataUpdateCoordinator( + coordinator: DataUpdateCoordinator = DataUpdateCoordinator( hass, _LOGGER, name=entry.title, diff --git a/mypy.ini b/mypy.ini index 22cd0c478d5..01bf959491e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1334,9 +1334,6 @@ ignore_errors = true [mypy-homeassistant.components.elkm1.*] ignore_errors = true -[mypy-homeassistant.components.emonitor.*] -ignore_errors = true - [mypy-homeassistant.components.enphase_envoy.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 31e364d6062..c3b135b4de6 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -37,7 +37,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.directv.*", "homeassistant.components.doorbird.*", "homeassistant.components.elkm1.*", - "homeassistant.components.emonitor.*", "homeassistant.components.enphase_envoy.*", "homeassistant.components.entur_public_transport.*", "homeassistant.components.evohome.*", From 355a067d842effa7f67dc5044be88ffdd891fef9 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 10 Aug 2021 10:55:38 +0200 Subject: [PATCH 112/355] Activate mypy for Smart Meter Texas (#53954) --- homeassistant/components/smart_meter_texas/__init__.py | 2 +- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/smart_meter_texas/__init__.py b/homeassistant/components/smart_meter_texas/__init__.py index 3e88221851b..7b500ed58e7 100644 --- a/homeassistant/components/smart_meter_texas/__init__.py +++ b/homeassistant/components/smart_meter_texas/__init__.py @@ -94,7 +94,7 @@ class SmartMeterTexasData: self.account = account websession = aiohttp_client.async_get_clientsession(hass) self.client = Client(websession, account) - self.meters = [] + self.meters: list = [] async def setup(self): """Fetch all of the user's meters.""" diff --git a/mypy.ini b/mypy.ini index 01bf959491e..dcedc8d57ef 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1631,9 +1631,6 @@ ignore_errors = true [mypy-homeassistant.components.sma.*] ignore_errors = true -[mypy-homeassistant.components.smart_meter_texas.*] -ignore_errors = true - [mypy-homeassistant.components.smartthings.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index c3b135b4de6..3e669998eab 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -136,7 +136,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.sesame.*", "homeassistant.components.sharkiq.*", "homeassistant.components.sma.*", - "homeassistant.components.smart_meter_texas.*", "homeassistant.components.smartthings.*", "homeassistant.components.smarttub.*", "homeassistant.components.smarty.*", From 814411dc1d7d14d5ff0499fd7e8f9804b817c490 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 10 Aug 2021 10:56:34 +0200 Subject: [PATCH 113/355] Activate mypy for Solar-Log (#53952) --- homeassistant/components/solarlog/config_flow.py | 2 +- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/solarlog/config_flow.py b/homeassistant/components/solarlog/config_flow.py index cced913222a..4267502e3ca 100644 --- a/homeassistant/components/solarlog/config_flow.py +++ b/homeassistant/components/solarlog/config_flow.py @@ -31,7 +31,7 @@ class SolarLogConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._errors = {} + self._errors: dict = {} def _host_in_configuration_exists(self, host) -> bool: """Return True if host exists in configuration.""" diff --git a/mypy.ini b/mypy.ini index dcedc8d57ef..3b6f040368d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1643,9 +1643,6 @@ ignore_errors = true [mypy-homeassistant.components.solaredge.*] ignore_errors = true -[mypy-homeassistant.components.solarlog.*] -ignore_errors = true - [mypy-homeassistant.components.somfy.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 3e669998eab..b507378db43 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -140,7 +140,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.smarttub.*", "homeassistant.components.smarty.*", "homeassistant.components.solaredge.*", - "homeassistant.components.solarlog.*", "homeassistant.components.somfy.*", "homeassistant.components.somfy_mylink.*", "homeassistant.components.sonarr.*", From a2a484045571ef248689848369d97b83794a52e7 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 10 Aug 2021 11:02:31 +0200 Subject: [PATCH 114/355] Using VCN install as action (#54383) --- .github/workflows/builder.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index abe0cfcb63e..25d4d0ca8a0 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -248,11 +248,12 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Install VCN tools + uses: home-assistant/actions/helpers/vcn@master + - name: Build Meta Image shell: bash run: | - bash <(curl https://getvcn.codenotary.com -L) - export DOCKER_CLI_EXPERIMENTAL=enabled function create_manifest() { From e5f884efd1042cbd0a391416957ddfe40a49d563 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 10 Aug 2021 11:48:16 +0200 Subject: [PATCH 115/355] Activate mypy for google_maps (#53725) --- .../components/google_maps/device_tracker.py | 13 ++++++++++--- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/google_maps/device_tracker.py b/homeassistant/components/google_maps/device_tracker.py index b6bd6f71bf4..1a0396a69ac 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -1,4 +1,6 @@ """Support for Google Maps location sharing.""" +from __future__ import annotations + from datetime import timedelta import logging @@ -6,7 +8,10 @@ from locationsharinglib import Service from locationsharinglib.locationsharinglibexceptions import InvalidCookies import voluptuous as vol -from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import ( + PLATFORM_SCHEMA as PLATFORM_SCHEMA_BASE, + SOURCE_TYPE_GPS, +) from homeassistant.const import ( ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, @@ -30,7 +35,9 @@ CONF_MAX_GPS_ACCURACY = "max_gps_accuracy" CREDENTIALS_FILE = ".google_maps_location_sharing.cookies" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +# the parent "device_tracker" have marked the schemas as legacy, so this +# need to be refactored as part of a bigger rewrite. +PLATFORM_SCHEMA = PLATFORM_SCHEMA_BASE.extend( { vol.Required(CONF_USERNAME): cv.string, vol.Optional(CONF_MAX_GPS_ACCURACY, default=100000): vol.Coerce(float), @@ -53,7 +60,7 @@ class GoogleMapsScanner: self.username = config[CONF_USERNAME] self.max_gps_accuracy = config[CONF_MAX_GPS_ACCURACY] self.scan_interval = config.get(CONF_SCAN_INTERVAL) or timedelta(seconds=60) - self._prev_seen = {} + self._prev_seen: dict[str, str] = {} credfile = f"{hass.config.path(CREDENTIALS_FILE)}.{slugify(self.username)}" try: diff --git a/mypy.ini b/mypy.ini index 3b6f040368d..3e6c14fb6a8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1370,9 +1370,6 @@ ignore_errors = true [mypy-homeassistant.components.google_assistant.*] ignore_errors = true -[mypy-homeassistant.components.google_maps.*] -ignore_errors = true - [mypy-homeassistant.components.google_pubsub.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index b507378db43..88c54b4c91e 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -49,7 +49,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.geniushub.*", "homeassistant.components.glances.*", "homeassistant.components.google_assistant.*", - "homeassistant.components.google_maps.*", "homeassistant.components.google_pubsub.*", "homeassistant.components.gpmdp.*", "homeassistant.components.gree.*", From 9c29d9f8eb6db0284a0e45447a3400ed0c8157ed Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 10 Aug 2021 12:36:20 +0200 Subject: [PATCH 116/355] Activate mypy for Proxmox VE (#53955) --- homeassistant/components/proxmoxve/__init__.py | 5 ++++- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py index 1b0d07c69a3..9c650363aad 100644 --- a/homeassistant/components/proxmoxve/__init__.py +++ b/homeassistant/components/proxmoxve/__init__.py @@ -1,4 +1,6 @@ """Support for Proxmox VE.""" +from __future__ import annotations + from datetime import timedelta import logging @@ -132,7 +134,8 @@ async def async_setup(hass: HomeAssistant, config: dict): await hass.async_add_executor_job(build_client) - coordinators = hass.data[DOMAIN][COORDINATORS] = {} + coordinators: dict[str, dict[str, dict[int, DataUpdateCoordinator]]] = {} + hass.data[DOMAIN][COORDINATORS] = coordinators # Create a coordinator for each vm/container for host_config in config[DOMAIN]: diff --git a/mypy.ini b/mypy.ini index 3e6c14fb6a8..a97ba87f16b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1592,9 +1592,6 @@ ignore_errors = true [mypy-homeassistant.components.profiler.*] ignore_errors = true -[mypy-homeassistant.components.proxmoxve.*] -ignore_errors = true - [mypy-homeassistant.components.rachio.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 88c54b4c91e..e100ffcea52 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -123,7 +123,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.plum_lightpad.*", "homeassistant.components.point.*", "homeassistant.components.profiler.*", - "homeassistant.components.proxmoxve.*", "homeassistant.components.rachio.*", "homeassistant.components.ring.*", "homeassistant.components.rpi_power.*", From 39d7bb4f1a3711fb9ccc5c3f74efa021e0c10adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 10 Aug 2021 13:11:12 +0200 Subject: [PATCH 117/355] Use `_attr_*` for Launch Library (#54388) --- .../components/launch_library/sensor.py | 58 +++++++------------ 1 file changed, 21 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/launch_library/sensor.py b/homeassistant/components/launch_library/sensor.py index 831e44dca8f..1d2f8ef0577 100644 --- a/homeassistant/components/launch_library/sensor.py +++ b/homeassistant/components/launch_library/sensor.py @@ -42,48 +42,32 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class LaunchLibrarySensor(SensorEntity): """Representation of a launch_library Sensor.""" - def __init__(self, launches: PyLaunches, name: str) -> None: + _attr_icon = "mdi:rocket" + + def __init__(self, api: PyLaunches, name: str) -> None: """Initialize the sensor.""" - self.launches = launches - self.next_launch = None - self._name = name + self.api = api + self._attr_name = name async def async_update(self) -> None: """Get the latest data.""" try: - launches = await self.launches.upcoming_launches() + launches = await self.api.upcoming_launches() except PyLaunchesException as exception: _LOGGER.error("Error getting data, %s", exception) + self._attr_available = False else: - if launches: - self.next_launch = launches[0] - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return self._name - - @property - def state(self) -> str | None: - """Return the state of the sensor.""" - if self.next_launch: - return self.next_launch.name - return None - - @property - def icon(self) -> str: - """Return the icon of the sensor.""" - return "mdi:rocket" - - @property - def extra_state_attributes(self) -> dict | None: - """Return attributes for the sensor.""" - if self.next_launch: - return { - ATTR_LAUNCH_TIME: self.next_launch.net, - ATTR_AGENCY: self.next_launch.launch_service_provider.name, - ATTR_AGENCY_COUNTRY_CODE: self.next_launch.pad.location.country_code, - ATTR_STREAM: self.next_launch.webcast_live, - ATTR_ATTRIBUTION: ATTRIBUTION, - } - return None + if launches and ( + next_launch := next((launch for launch in launches), None) + ): + self._attr_available = True + self._attr_state = next_launch.name + self._attr_extra_state_attributes.update( + { + ATTR_LAUNCH_TIME: next_launch.net, + ATTR_AGENCY: next_launch.launch_service_provider.name, + ATTR_AGENCY_COUNTRY_CODE: next_launch.pad.location.country_code, + ATTR_STREAM: next_launch.webcast_live, + ATTR_ATTRIBUTION: ATTRIBUTION, + } + ) From a7c08fff813bbffe3bb17b152ca47cd230c604a6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 11 Aug 2021 00:06:27 +1200 Subject: [PATCH 118/355] Apply suggested changes to tidy juicenet sensor code (#54390) --- homeassistant/components/juicenet/sensor.py | 25 +-------------------- 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/homeassistant/components/juicenet/sensor.py b/homeassistant/components/juicenet/sensor.py index 2b8bd61e1fb..435508f823d 100644 --- a/homeassistant/components/juicenet/sensor.py +++ b/homeassistant/components/juicenet/sensor.py @@ -32,7 +32,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="temperature", name="Temperature", unit_of_measurement=TEMP_CELSIUS, - icon="mdi:thermometer", device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), @@ -40,14 +39,12 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="voltage", name="Voltage", unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - icon="mdi:flash", device_class=DEVICE_CLASS_VOLTAGE, ), SensorEntityDescription( key="amps", name="Amps", unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - icon="mdi:flash", device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, ), @@ -55,7 +52,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="watts", name="Watts", unit_of_measurement=POWER_WATT, - icon="mdi:flash", device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), @@ -69,7 +65,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="energy_added", name="Energy added", unit_of_measurement=ENERGY_WATT_HOUR, - icon="mdi:flash", device_class=DEVICE_CLASS_ENERGY, ), ) @@ -117,22 +112,4 @@ class JuiceNetSensorDevice(JuiceNetDevice, SensorEntity): @property def state(self): """Return the state.""" - state = None - sensor_type = self.entity_description.key - if sensor_type == "status": - state = self.device.status - elif sensor_type == "temperature": - state = self.device.temperature - elif sensor_type == "voltage": - state = self.device.voltage - elif sensor_type == "amps": - state = self.device.amps - elif sensor_type == "watts": - state = self.device.watts - elif sensor_type == "charge_time": - state = self.device.charge_time - elif sensor_type == "energy_added": - state = self.device.energy_added - else: - state = "Unknown" - return state + return getattr(self.device, self.entity_description.key, None) From 5de1adacf79036938c90863e81e3fbb3363b55f9 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 10 Aug 2021 14:55:11 +0200 Subject: [PATCH 119/355] Xiaomi miio add coordinator to fan platform (#54366) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * init coordinator for airpurifiers and airfresh * Update fan entities with coordinator * cache mode and fan_level at user update * pylint define attributes in _init * Update homeassistant/components/xiaomi_miio/fan.py Co-authored-by: Maciej Bieniek * Update homeassistant/components/xiaomi_miio/fan.py Co-authored-by: Maciej Bieniek * Update homeassistant/components/xiaomi_miio/fan.py Co-authored-by: Maciej Bieniek * cleanup code * Set hass.data[DATA_KEY] to enable * rename to filtered_entities in service handler * Update homeassistant/components/xiaomi_miio/fan.py Co-authored-by: Joakim Sørensen * flake Co-authored-by: Maciej Bieniek Co-authored-by: Joakim Sørensen --- .../components/xiaomi_miio/__init__.py | 58 +++- homeassistant/components/xiaomi_miio/fan.py | 316 +++++++----------- 2 files changed, 176 insertions(+), 198 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index bd9e69bd12d..faff2194948 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -3,7 +3,15 @@ from datetime import timedelta import logging import async_timeout -from miio import AirHumidifier, AirHumidifierMiot, AirHumidifierMjjsq, DeviceException +from miio import ( + AirFresh, + AirHumidifier, + AirHumidifierMiot, + AirHumidifierMjjsq, + AirPurifier, + AirPurifierMiot, + DeviceException, +) from miio.gateway.gateway import GatewayException from homeassistant import config_entries, core @@ -23,10 +31,13 @@ from .const import ( KEY_DEVICE, MODELS_AIR_MONITOR, MODELS_FAN, + MODELS_FAN_MIIO, MODELS_HUMIDIFIER, + MODELS_HUMIDIFIER_MIIO, MODELS_HUMIDIFIER_MIOT, MODELS_HUMIDIFIER_MJJSQ, MODELS_LIGHT, + MODELS_PURIFIER_MIOT, MODELS_SWITCH, MODELS_VACUUM, ) @@ -107,27 +118,52 @@ async def async_create_miio_device_and_coordinator( token = entry.data[CONF_TOKEN] name = entry.title device = None + migrate = False - if model not in MODELS_HUMIDIFIER: + if ( + model not in MODELS_HUMIDIFIER + and model not in MODELS_PURIFIER_MIOT + and model not in MODELS_FAN_MIIO + ): return _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) + # Humidifiers if model in MODELS_HUMIDIFIER_MIOT: device = AirHumidifierMiot(host, token) + migrate = True elif model in MODELS_HUMIDIFIER_MJJSQ: device = AirHumidifierMjjsq(host, token, model=model) - else: + migrate = True + elif model in MODELS_HUMIDIFIER_MIIO: device = AirHumidifier(host, token, model=model) + migrate = True + # Airpurifiers and Airfresh + elif model in MODELS_PURIFIER_MIOT: + device = AirPurifierMiot(host, token) + elif model.startswith("zhimi.airpurifier."): + device = AirPurifier(host, token) + elif model.startswith("zhimi.airfresh."): + device = AirFresh(host, token) + else: + _LOGGER.error( + "Unsupported device found! Please create an issue at " + "https://github.com/syssi/xiaomi_airpurifier/issues " + "and provide the following data: %s", + model, + ) + return - # Removing fan platform entity for humidifiers and migrate the name to the config entry for migration - entity_registry = er.async_get(hass) - entity_id = entity_registry.async_get_entity_id("fan", DOMAIN, entry.unique_id) - if entity_id: - # This check is entities that have a platform migration only and should be removed in the future - if migrate_entity_name := entity_registry.async_get(entity_id).name: - hass.config_entries.async_update_entry(entry, title=migrate_entity_name) - entity_registry.async_remove(entity_id) + if migrate: + # Removing fan platform entity for humidifiers and migrate the name to the config entry for migration + entity_registry = er.async_get(hass) + entity_id = entity_registry.async_get_entity_id("fan", DOMAIN, entry.unique_id) + if entity_id: + # This check is entities that have a platform migration only and should be removed in the future + if migrate_entity_name := entity_registry.async_get(entity_id).name: + hass.config_entries.async_update_entry(entry, title=migrate_entity_name) + entity_registry.async_remove(entity_id) async def async_update_data(): """Fetch data from the device using async_add_executor_job.""" diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index feeadf2bccc..fe4df2cd6d3 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -1,11 +1,9 @@ """Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier.""" import asyncio from enum import Enum -from functools import partial import logging import math -from miio import AirFresh, AirPurifier, AirPurifierMiot, DeviceException from miio.airfresh import ( LedBrightness as AirfreshLedBrightness, OperationMode as AirfreshOperationMode, @@ -35,6 +33,7 @@ from homeassistant.const import ( CONF_NAME, CONF_TOKEN, ) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.util.percentage import ( percentage_to_ranged_value, @@ -56,6 +55,8 @@ from .const import ( FEATURE_SET_LED, FEATURE_SET_LED_BRIGHTNESS, FEATURE_SET_VOLUME, + KEY_COORDINATOR, + KEY_DEVICE, MODEL_AIRPURIFIER_2H, MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_PRO, @@ -79,9 +80,8 @@ from .const import ( SERVICE_SET_LEARN_MODE_ON, SERVICE_SET_LED_BRIGHTNESS, SERVICE_SET_VOLUME, - SUCCESS, ) -from .device import XiaomiMiioEntity +from .device import XiaomiCoordinatedMiioEntity _LOGGER = logging.getLogger(__name__) @@ -430,94 +430,89 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fan from a config entry.""" entities = [] - if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {} + if not config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: + return - host = config_entry.data[CONF_HOST] - token = config_entry.data[CONF_TOKEN] - name = config_entry.title - model = config_entry.data[CONF_MODEL] - unique_id = config_entry.unique_id + hass.data.setdefault(DATA_KEY, {}) - _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) + name = config_entry.title + model = config_entry.data[CONF_MODEL] + unique_id = config_entry.unique_id + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] - if model in MODELS_PURIFIER_MIOT: - air_purifier = AirPurifierMiot(host, token) - entity = XiaomiAirPurifierMiot( - name, air_purifier, config_entry, unique_id, allowed_failures=2 - ) - elif model.startswith("zhimi.airpurifier."): - air_purifier = AirPurifier(host, token) - entity = XiaomiAirPurifier(name, air_purifier, config_entry, unique_id) - elif model.startswith("zhimi.airfresh."): - air_fresh = AirFresh(host, token) - entity = XiaomiAirFresh(name, air_fresh, config_entry, unique_id) + if model in MODELS_PURIFIER_MIOT: + entity = XiaomiAirPurifierMiot( + name, + device, + config_entry, + unique_id, + coordinator, + ) + elif model.startswith("zhimi.airpurifier."): + entity = XiaomiAirPurifier(name, device, config_entry, unique_id, coordinator) + elif model.startswith("zhimi.airfresh."): + entity = XiaomiAirFresh(name, device, config_entry, unique_id, coordinator) + else: + return + + hass.data[DATA_KEY][unique_id] = entity + + entities.append(entity) + + async def async_service_handler(service): + """Map services to methods on XiaomiAirPurifier.""" + method = SERVICE_TO_METHOD[service.service] + params = { + key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID + } + entity_ids = service.data.get(ATTR_ENTITY_ID) + if entity_ids: + filtered_entities = [ + entity + for entity in hass.data[DATA_KEY].values() + if entity.entity_id in entity_ids + ] else: - _LOGGER.error( - "Unsupported device found! Please create an issue at " - "https://github.com/syssi/xiaomi_airpurifier/issues " - "and provide the following data: %s", - model, - ) - return + filtered_entities = hass.data[DATA_KEY].values() - hass.data[DATA_KEY][host] = entity - entities.append(entity) + update_tasks = [] - async def async_service_handler(service): - """Map services to methods on XiaomiAirPurifier.""" - method = SERVICE_TO_METHOD[service.service] - params = { - key: value - for key, value in service.data.items() - if key != ATTR_ENTITY_ID - } - entity_ids = service.data.get(ATTR_ENTITY_ID) - if entity_ids: - entities = [ - entity - for entity in hass.data[DATA_KEY].values() - if entity.entity_id in entity_ids - ] - else: - entities = hass.data[DATA_KEY].values() - - update_tasks = [] - - for entity in entities: - entity_method = getattr(entity, method["method"], None) - if not entity_method: - continue - await entity_method(**params) - update_tasks.append( - hass.async_create_task(entity.async_update_ha_state(True)) - ) - - if update_tasks: - await asyncio.wait(update_tasks) - - for air_purifier_service, method in SERVICE_TO_METHOD.items(): - schema = method.get("schema", AIRPURIFIER_SERVICE_SCHEMA) - hass.services.async_register( - DOMAIN, air_purifier_service, async_service_handler, schema=schema + for entity in filtered_entities: + entity_method = getattr(entity, method["method"], None) + if not entity_method: + continue + await entity_method(**params) + update_tasks.append( + hass.async_create_task(entity.async_update_ha_state(True)) ) - async_add_entities(entities, update_before_add=True) + if update_tasks: + await asyncio.wait(update_tasks) + + for air_purifier_service, method in SERVICE_TO_METHOD.items(): + schema = method.get("schema", AIRPURIFIER_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, air_purifier_service, async_service_handler, schema=schema + ) + + async_add_entities(entities) -class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity): +class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): """Representation of a generic Xiaomi device.""" - def __init__(self, name, device, entry, unique_id): + def __init__(self, name, device, entry, unique_id, coordinator): """Initialize the generic Xiaomi device.""" - super().__init__(name, device, entry, unique_id) + super().__init__(name, device, entry, unique_id, coordinator) self._available = False + self._available_attributes = {} self._state = None + self._mode = None + self._fan_level = None self._state_attrs = {ATTR_MODEL: self._model} self._device_features = FEATURE_SET_CHILD_LOCK - self._skip_update = False self._supported_features = 0 self._speed_count = 100 self._preset_modes = [] @@ -583,22 +578,20 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity): return value - async def _try_command(self, mask_error, func, *args, **kwargs): - """Call a miio device command handling error messages.""" - try: - result = await self.hass.async_add_executor_job( - partial(func, *args, **kwargs) - ) - - _LOGGER.debug("Response received from miio device: %s", result) - - return result == SUCCESS - except DeviceException as exc: - if self._available: - _LOGGER.error(mask_error, exc) - self._available = False - - return False + @callback + def _handle_coordinator_update(self): + """Fetch state from the device.""" + self._available = True + self._state = self.coordinator.data.is_on + self._state_attrs.update( + { + key: self._extract_value_from_attribute(self.coordinator.data, value) + for key, value in self._available_attributes.items() + } + ) + self._mode = self._state_attrs.get(ATTR_MODE) + self._fan_level = self._state_attrs.get(ATTR_FAN_LEVEL) + self.async_write_ha_state() # # The fan entity model has changed to use percentages and preset_modes @@ -630,7 +623,7 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity): if result: self._state = True - self._skip_update = True + self.async_write_ha_state() async def async_turn_off(self, **kwargs) -> None: """Turn the device off.""" @@ -640,7 +633,7 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity): if result: self._state = False - self._skip_update = True + self.async_write_ha_state() async def async_set_buzzer_on(self): """Turn the buzzer on.""" @@ -706,11 +699,9 @@ class XiaomiAirPurifier(XiaomiGenericDevice): REVERSE_SPEED_MODE_MAPPING = {v: k for k, v in SPEED_MODE_MAPPING.items()} - def __init__(self, name, device, entry, unique_id, allowed_failures=0): + def __init__(self, name, device, entry, unique_id, coordinator): """Initialize the plug switch.""" - super().__init__(name, device, entry, unique_id) - self._allowed_failures = allowed_failures - self._failure = 0 + super().__init__(name, device, entry, unique_id, coordinator) if self._model == MODEL_AIRPURIFIER_PRO: self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO @@ -774,45 +765,8 @@ class XiaomiAirPurifier(XiaomiGenericDevice): self._state_attrs.update( {attribute: None for attribute in self._available_attributes} ) - - async def async_update(self): - """Fetch state from the device.""" - # On state change the device doesn't provide the new state immediately. - if self._skip_update: - self._skip_update = False - return - - try: - state = await self.hass.async_add_executor_job(self._device.status) - _LOGGER.debug("Got new state: %s", state) - - self._available = True - self._state = state.is_on - self._state_attrs.update( - { - key: self._extract_value_from_attribute(state, value) - for key, value in self._available_attributes.items() - } - ) - - self._failure = 0 - - except DeviceException as ex: - self._failure += 1 - if self._failure < self._allowed_failures: - _LOGGER.info( - "Got exception while fetching the state: %s, failure: %d", - ex, - self._failure, - ) - else: - if self._available: - self._available = False - _LOGGER.error( - "Got exception while fetching the state: %s, failure: %d", - ex, - self._failure, - ) + self._mode = self._state_attrs.get(ATTR_MODE) + self._fan_level = self._state_attrs.get(ATTR_FAN_LEVEL) @property def preset_mode(self): @@ -1032,8 +986,7 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): def percentage(self): """Return the current percentage based speed.""" if self._state: - fan_level = self._state_attrs[ATTR_FAN_LEVEL] - return ranged_value_to_percentage((1, 3), fan_level) + return ranged_value_to_percentage((1, 3), self._fan_level) return None @@ -1041,9 +994,7 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): def preset_mode(self): """Get the active preset mode.""" if self._state: - preset_mode = AirpurifierMiotOperationMode( - self._state_attrs[ATTR_MODE] - ).name + preset_mode = AirpurifierMiotOperationMode(self._mode).name return preset_mode if preset_mode in self._preset_modes else None return None @@ -1053,7 +1004,7 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): def speed(self): """Return the current speed.""" if self._state: - return AirpurifierMiotOperationMode(self._state_attrs[ATTR_MODE]).name + return AirpurifierMiotOperationMode(self._mode).name return None @@ -1063,12 +1014,15 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): This method is a coroutine. """ fan_level = math.ceil(percentage_to_ranged_value((1, 3), percentage)) - if fan_level: - await self._try_command( - "Setting fan level of the miio device failed.", - self._device.set_fan_level, - fan_level, - ) + if not fan_level: + return + if await self._try_command( + "Setting fan level of the miio device failed.", + self._device.set_fan_level, + fan_level, + ): + self._fan_level = fan_level + self.async_write_ha_state() async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of the fan. @@ -1078,11 +1032,13 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): if preset_mode not in self.preset_modes: _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) return - await self._try_command( + if await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, self.PRESET_MODE_MAPPING[preset_mode], - ) + ): + self._mode = self.PRESET_MODE_MAPPING[preset_mode].value + self.async_write_ha_state() # the async_set_speed function is deprecated, support will end with release 2021.7 # it is added here only for compatibility with legacy speeds @@ -1093,11 +1049,13 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): _LOGGER.debug("Setting the operation mode to: %s", speed) - await self._try_command( + if await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, AirpurifierMiotOperationMode[speed.title()], - ) + ): + self._mode = AirpurifierMiotOperationMode[speed.title()].value + self.async_write_ha_state() async def async_set_led_brightness(self, brightness: int = 2): """Set the led brightness.""" @@ -1128,9 +1086,9 @@ class XiaomiAirFresh(XiaomiGenericDevice): "Interval": AirfreshOperationMode.Interval, } - def __init__(self, name, device, entry, unique_id): + def __init__(self, name, device, entry, unique_id, coordinator): """Initialize the miio device.""" - super().__init__(name, device, entry, unique_id) + super().__init__(name, device, entry, unique_id, coordinator) self._device_features = FEATURE_FLAGS_AIRFRESH self._available_attributes = AVAILABLE_ATTRIBUTES_AIRFRESH @@ -1142,37 +1100,13 @@ class XiaomiAirFresh(XiaomiGenericDevice): self._state_attrs.update( {attribute: None for attribute in self._available_attributes} ) - - async def async_update(self): - """Fetch state from the device.""" - # On state change the device doesn't provide the new state immediately. - if self._skip_update: - self._skip_update = False - return - - try: - state = await self.hass.async_add_executor_job(self._device.status) - _LOGGER.debug("Got new state: %s", state) - - self._available = True - self._state = state.is_on - self._state_attrs.update( - { - key: self._extract_value_from_attribute(state, value) - for key, value in self._available_attributes.items() - } - ) - - except DeviceException as ex: - if self._available: - self._available = False - _LOGGER.error("Got exception while fetching the state: %s", ex) + self._mode = self._state_attrs.get(ATTR_MODE) @property def preset_mode(self): """Get the active preset mode.""" if self._state: - preset_mode = AirfreshOperationMode(self._state_attrs[ATTR_MODE]).name + preset_mode = AirfreshOperationMode(self._mode).name return preset_mode if preset_mode in self._preset_modes else None return None @@ -1181,7 +1115,7 @@ class XiaomiAirFresh(XiaomiGenericDevice): def percentage(self): """Return the current percentage based speed.""" if self._state: - mode = AirfreshOperationMode(self._state_attrs[ATTR_MODE]) + mode = AirfreshOperationMode(self._mode) if mode in self.REVERSE_SPEED_MODE_MAPPING: return ranged_value_to_percentage( (1, self._speed_count), self.REVERSE_SPEED_MODE_MAPPING[mode] @@ -1194,7 +1128,7 @@ class XiaomiAirFresh(XiaomiGenericDevice): def speed(self): """Return the current speed.""" if self._state: - return AirfreshOperationMode(self._state_attrs[ATTR_MODE]).name + return AirfreshOperationMode(self._mode).name return None @@ -1207,11 +1141,15 @@ class XiaomiAirFresh(XiaomiGenericDevice): percentage_to_ranged_value((1, self._speed_count), percentage) ) if speed_mode: - await self._try_command( + if await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, AirfreshOperationMode(self.SPEED_MODE_MAPPING[speed_mode]), - ) + ): + self._mode = AirfreshOperationMode( + self.SPEED_MODE_MAPPING[speed_mode] + ).value + self.async_write_ha_state() async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of the fan. @@ -1221,11 +1159,13 @@ class XiaomiAirFresh(XiaomiGenericDevice): if preset_mode not in self.preset_modes: _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) return - await self._try_command( + if await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, self.PRESET_MODE_MAPPING[preset_mode], - ) + ): + self._mode = self.PRESET_MODE_MAPPING[preset_mode].value + self.async_write_ha_state() # the async_set_speed function is deprecated, support will end with release 2021.7 # it is added here only for compatibility with legacy speeds @@ -1236,11 +1176,13 @@ class XiaomiAirFresh(XiaomiGenericDevice): _LOGGER.debug("Setting the operation mode to: %s", speed) - await self._try_command( + if await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, AirfreshOperationMode[speed.title()], - ) + ): + self._mode = AirfreshOperationMode[speed.title()].value + self.async_write_ha_state() async def async_set_led_on(self): """Turn the led on.""" From 1d40a6e40717943420e545f1a8f639f573bd322d Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 10 Aug 2021 14:57:57 +0200 Subject: [PATCH 120/355] Activate mypy from amcrest and make the needed changes (#54392) --- homeassistant/components/amcrest/binary_sensor.py | 4 ++-- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py index 0add382b81f..98e0be73ef4 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -53,7 +53,7 @@ _CROSSLINE_DETECTED_PARAMS = ( DEVICE_CLASS_MOTION, "CrossLineDetection", ) -BINARY_SENSORS = { +RAW_BINARY_SENSORS = { BINARY_SENSOR_AUDIO_DETECTED: _AUDIO_DETECTED_PARAMS, BINARY_SENSOR_AUDIO_DETECTED_POLLED: _AUDIO_DETECTED_PARAMS, BINARY_SENSOR_MOTION_DETECTED: _MOTION_DETECTED_PARAMS, @@ -64,7 +64,7 @@ BINARY_SENSORS = { } BINARY_SENSORS = { k: dict(zip((SENSOR_NAME, SENSOR_DEVICE_CLASS, SENSOR_EVENT_CODE), v)) - for k, v in BINARY_SENSORS.items() + for k, v in RAW_BINARY_SENSORS.items() } _EXCLUSIVE_OPTIONS = [ {BINARY_SENSOR_MOTION_DETECTED, BINARY_SENSOR_MOTION_DETECTED_POLLED}, diff --git a/mypy.ini b/mypy.ini index a97ba87f16b..6fffe2bc3c1 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1274,9 +1274,6 @@ ignore_errors = true [mypy-homeassistant.components.almond.*] ignore_errors = true -[mypy-homeassistant.components.amcrest.*] -ignore_errors = true - [mypy-homeassistant.components.analytics.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index e100ffcea52..9747a5ee8c0 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -17,7 +17,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.adguard.*", "homeassistant.components.aemet.*", "homeassistant.components.almond.*", - "homeassistant.components.amcrest.*", "homeassistant.components.analytics.*", "homeassistant.components.asuswrt.*", "homeassistant.components.atag.*", From 8ea5a0dbc194d5a7e7f09f7df2eee5c5871e434c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 10 Aug 2021 15:03:12 +0200 Subject: [PATCH 121/355] Remove useless check in launch_library (#54393) --- .../components/launch_library/sensor.py | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/launch_library/sensor.py b/homeassistant/components/launch_library/sensor.py index 1d2f8ef0577..68d2a024bca 100644 --- a/homeassistant/components/launch_library/sensor.py +++ b/homeassistant/components/launch_library/sensor.py @@ -57,17 +57,13 @@ class LaunchLibrarySensor(SensorEntity): _LOGGER.error("Error getting data, %s", exception) self._attr_available = False else: - if launches and ( - next_launch := next((launch for launch in launches), None) - ): + if next_launch := next((launch for launch in launches), None): self._attr_available = True self._attr_state = next_launch.name - self._attr_extra_state_attributes.update( - { - ATTR_LAUNCH_TIME: next_launch.net, - ATTR_AGENCY: next_launch.launch_service_provider.name, - ATTR_AGENCY_COUNTRY_CODE: next_launch.pad.location.country_code, - ATTR_STREAM: next_launch.webcast_live, - ATTR_ATTRIBUTION: ATTRIBUTION, - } - ) + self._attr_extra_state_attributes = { + ATTR_LAUNCH_TIME: next_launch.net, + ATTR_AGENCY: next_launch.launch_service_provider.name, + ATTR_AGENCY_COUNTRY_CODE: next_launch.pad.location.country_code, + ATTR_STREAM: next_launch.webcast_live, + ATTR_ATTRIBUTION: ATTRIBUTION, + } From cf8f27bb44420f4cf45054998872ffd0265bcb61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 10 Aug 2021 15:03:34 +0200 Subject: [PATCH 122/355] Adjust version tests (#54391) * Adjust version tests * patch local import --- tests/components/version/test_sensor.py | 60 +++++++++++++++++++++---- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/tests/components/version/test_sensor.py b/tests/components/version/test_sensor.py index 1f64fe23039..c8883e72389 100644 --- a/tests/components/version/test_sensor.py +++ b/tests/components/version/test_sensor.py @@ -1,31 +1,49 @@ """The test for the version sensor platform.""" +from datetime import timedelta from unittest.mock import patch from pyhaversion import HaVersionSource, exceptions as pyhaversionexceptions import pytest -from homeassistant.components.version.sensor import ALL_SOURCES +from homeassistant.components.version.sensor import HA_VERSION_SOURCES from homeassistant.setup import async_setup_component +from homeassistant.util import dt + +from tests.common import async_fire_time_changed MOCK_VERSION = "10.0" @pytest.mark.parametrize( - "source", - ALL_SOURCES, + "source,target_source,name", + ( + ( + ("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 + ) + ), ) -async def test_version_source(hass, source): +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("pyhaversion.version.HaVersion.version", MOCK_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() - name = "current_version" if source == HaVersionSource.LOCAL else "latest_version" state = hass.states.get(f"sensor.{name}") + assert state + assert state.attributes["source"] == target_source assert state.state == MOCK_VERSION @@ -34,7 +52,7 @@ async def test_version_fetch_exception(hass, caplog): """Test fetch exception thrown during updates.""" config = {"sensor": {"platform": "version"}} with patch( - "pyhaversion.version.HaVersion.get_version", + "homeassistant.components.version.sensor.HaVersion.get_version", side_effect=pyhaversionexceptions.HaVersionFetchException( "Fetch exception from pyhaversion" ), @@ -48,9 +66,35 @@ async def test_version_parse_exception(hass, caplog): """Test parse exception thrown during updates.""" config = {"sensor": {"platform": "version"}} with patch( - "pyhaversion.version.HaVersion.get_version", + "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" From f03b160c4637507691480de717b3f2d78e6ab844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 10 Aug 2021 16:28:39 +0200 Subject: [PATCH 123/355] Mill cleanup (#54396) --- homeassistant/components/mill/sensor.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index 8b68d0ebe38..bdd1a90fb38 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -5,7 +5,7 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, SensorEntity, ) -from homeassistant.const import ENERGY_KILO_WATT_HOUR, STATE_UNKNOWN +from homeassistant.const import ENERGY_KILO_WATT_HOUR from homeassistant.util import dt as dt_util from .const import CONSUMPTION_TODAY, CONSUMPTION_YEAR, DOMAIN, MANUFACTURER @@ -16,13 +16,12 @@ async def async_setup_entry(hass, entry, async_add_entities): mill_data_connection = hass.data[DOMAIN] - dev = [] - for heater in mill_data_connection.heaters.values(): - for sensor_type in (CONSUMPTION_TODAY, CONSUMPTION_YEAR): - dev.append( - MillHeaterEnergySensor(heater, mill_data_connection, sensor_type) - ) - async_add_entities(dev) + entities = [ + MillHeaterEnergySensor(heater, mill_data_connection, sensor_type) + for sensor_type in (CONSUMPTION_TODAY, CONSUMPTION_YEAR) + for heater in mill_data_connection.heaters.values() + ] + async_add_entities(entities) class MillHeaterEnergySensor(SensorEntity): @@ -71,7 +70,7 @@ class MillHeaterEnergySensor(SensorEntity): self._attr_state = _state return - if self.state not in [STATE_UNKNOWN, None] and _state < self.state: + if self.state is not None and _state < self.state: if self._sensor_type == CONSUMPTION_TODAY: self._attr_last_reset = dt_util.as_utc( dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) From d1ea38e8f0018a727c2a4da86c2f22faeb0a14d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 10 Aug 2021 16:29:51 +0200 Subject: [PATCH 124/355] Add 100% test coverage for Uptime Robot (#54314) * Add 100% test coverage for Uptime Robot * Update tests/components/uptimerobot/test_binary_sensor.py Co-authored-by: Martin Hjelmare * Add more typehints Co-authored-by: Martin Hjelmare --- .coveragerc | 4 - .../components/uptimerobot/binary_sensor.py | 2 +- .../components/uptimerobot/entity.py | 14 +- tests/components/uptimerobot/common.py | 95 ++++++++ .../uptimerobot/test_binary_sensor.py | 82 +++++++ .../uptimerobot/test_config_flow.py | 218 ++++++------------ tests/components/uptimerobot/test_init.py | 157 +++++++++---- 7 files changed, 381 insertions(+), 191 deletions(-) create mode 100644 tests/components/uptimerobot/common.py create mode 100644 tests/components/uptimerobot/test_binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 5d9c5e9c5c8..8088bbece78 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1115,10 +1115,6 @@ omit = homeassistant/components/upcloud/switch.py homeassistant/components/upnp/* homeassistant/components/upc_connect/* - homeassistant/components/uptimerobot/__init__.py - homeassistant/components/uptimerobot/binary_sensor.py - homeassistant/components/uptimerobot/const.py - homeassistant/components/uptimerobot/entity.py homeassistant/components/uscis/sensor.py homeassistant/components/vallox/* homeassistant/components/vasttrafik/sensor.py diff --git a/homeassistant/components/uptimerobot/binary_sensor.py b/homeassistant/components/uptimerobot/binary_sensor.py index f99689f2507..ac0dc0c1186 100644 --- a/homeassistant/components/uptimerobot/binary_sensor.py +++ b/homeassistant/components/uptimerobot/binary_sensor.py @@ -53,7 +53,7 @@ async def async_setup_entry( name=monitor.friendly_name, device_class=DEVICE_CLASS_CONNECTIVITY, ), - target=monitor.url, + monitor=monitor, ) for monitor in coordinator.data ], diff --git a/homeassistant/components/uptimerobot/entity.py b/homeassistant/components/uptimerobot/entity.py index b9783c88b9c..8ef60b3848b 100644 --- a/homeassistant/components/uptimerobot/entity.py +++ b/homeassistant/components/uptimerobot/entity.py @@ -20,11 +20,12 @@ class UptimeRobotEntity(CoordinatorEntity): self, coordinator: DataUpdateCoordinator, description: EntityDescription, - target: str, + monitor: UptimeRobotMonitor, ) -> None: """Initialize Uptime Robot entities.""" super().__init__(coordinator) self.entity_description = description + self._monitor = monitor self._attr_device_info = { "identifiers": {(DOMAIN, str(self.monitor.id))}, "name": "Uptime Robot", @@ -34,7 +35,7 @@ class UptimeRobotEntity(CoordinatorEntity): } self._attr_extra_state_attributes = { ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_TARGET: target, + ATTR_TARGET: self.monitor.url, } self._attr_unique_id = str(self.monitor.id) @@ -47,9 +48,12 @@ class UptimeRobotEntity(CoordinatorEntity): def monitor(self) -> UptimeRobotMonitor: """Return the monitor for this entity.""" return next( - monitor - for monitor in self._monitors - if str(monitor.id) == self.entity_description.key + ( + monitor + for monitor in self._monitors + if str(monitor.id) == self.entity_description.key + ), + self._monitor, ) @property diff --git a/tests/components/uptimerobot/common.py b/tests/components/uptimerobot/common.py new file mode 100644 index 00000000000..aa241ce5a92 --- /dev/null +++ b/tests/components/uptimerobot/common.py @@ -0,0 +1,95 @@ +"""Common constants and functions for Uptime Robot tests.""" +from __future__ import annotations + +from enum import Enum +from typing import Any +from unittest.mock import patch + +from pyuptimerobot import ( + APIStatus, + UptimeRobotAccount, + UptimeRobotApiError, + UptimeRobotApiResponse, + UptimeRobotMonitor, +) + +from homeassistant import config_entries +from homeassistant.components.uptimerobot.const import DOMAIN +from homeassistant.const import STATE_ON +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +MOCK_UPTIMEROBOT_API_KEY = "1234" +MOCK_UPTIMEROBOT_UNIQUE_ID = "1234567890" + +MOCK_UPTIMEROBOT_ACCOUNT = {"email": "test@test.test", "user_id": 1234567890} +MOCK_UPTIMEROBOT_ERROR = {"message": "test error from API."} +MOCK_UPTIMEROBOT_MONITOR = { + "id": 1234, + "friendly_name": "Test monitor", + "status": 2, + "type": 1, + "url": "http://example.com", +} + +MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA = { + "domain": DOMAIN, + "title": "test@test.test", + "data": {"platform": DOMAIN, "api_key": MOCK_UPTIMEROBOT_API_KEY}, + "unique_id": MOCK_UPTIMEROBOT_UNIQUE_ID, + "source": config_entries.SOURCE_USER, +} + +UPTIMEROBOT_TEST_ENTITY = "binary_sensor.test_monitor" + + +class MockApiResponseKey(str, Enum): + """Mock API response key.""" + + ACCOUNT = "account" + ERROR = "error" + MONITORS = "monitors" + + +def mock_uptimerobot_api_response( + data: dict[str, Any] + | None + | list[UptimeRobotMonitor] + | UptimeRobotAccount + | UptimeRobotApiError = None, + status: APIStatus = APIStatus.OK, + key: MockApiResponseKey = MockApiResponseKey.MONITORS, +) -> UptimeRobotApiResponse: + """Mock API response for Uptime Robot.""" + return UptimeRobotApiResponse.from_dict( + { + "stat": {"error": APIStatus.FAIL}.get(key, status), + key: data + if data is not None + else { + "account": MOCK_UPTIMEROBOT_ACCOUNT, + "error": MOCK_UPTIMEROBOT_ERROR, + "monitors": [MOCK_UPTIMEROBOT_MONITOR], + }.get(key, {}), + } + ) + + +async def setup_uptimerobot_integration(hass: HomeAssistant) -> MockConfigEntry: + """Set up the Uptime Robot integration.""" + mock_entry = MockConfigEntry(**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA) + mock_entry.add_to_hass(hass) + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + return_value=mock_uptimerobot_api_response(data=[MOCK_UPTIMEROBOT_MONITOR]), + ): + + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_ON + assert mock_entry.state == config_entries.ConfigEntryState.LOADED + + return mock_entry diff --git a/tests/components/uptimerobot/test_binary_sensor.py b/tests/components/uptimerobot/test_binary_sensor.py new file mode 100644 index 00000000000..13bb3b342e9 --- /dev/null +++ b/tests/components/uptimerobot/test_binary_sensor.py @@ -0,0 +1,82 @@ +"""Test Uptime Robot binary_sensor.""" + +from unittest.mock import patch + +from pyuptimerobot import UptimeRobotAuthenticationException + +from homeassistant.components.binary_sensor import DEVICE_CLASS_CONNECTIVITY +from homeassistant.components.uptimerobot.const import ( + ATTRIBUTION, + COORDINATOR_UPDATE_INTERVAL, + DOMAIN, +) +from homeassistant.const import STATE_ON, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +from homeassistant.util import dt + +from .common import ( + MOCK_UPTIMEROBOT_API_KEY, + MOCK_UPTIMEROBOT_MONITOR, + UPTIMEROBOT_TEST_ENTITY, + MockApiResponseKey, + mock_uptimerobot_api_response, + setup_uptimerobot_integration, +) + +from tests.common import async_fire_time_changed + + +async def test_config_import(hass: HomeAssistant) -> None: + """Test importing YAML configuration.""" + config = { + "binary_sensor": { + "platform": DOMAIN, + "api_key": MOCK_UPTIMEROBOT_API_KEY, + } + } + with patch( + "pyuptimerobot.UptimeRobot.async_get_account_details", + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), + ), patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + return_value=mock_uptimerobot_api_response(), + ): + assert await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + config_entries = hass.config_entries.async_entries(DOMAIN) + + assert len(config_entries) == 1 + config_entry = config_entries[0] + assert config_entry.source == "import" + + +async def test_presentation(hass: HomeAssistant) -> None: + """Test the presenstation of Uptime Robot binary_sensors.""" + await setup_uptimerobot_integration(hass) + + entity = hass.states.get(UPTIMEROBOT_TEST_ENTITY) + + assert entity.state == STATE_ON + assert entity.attributes["device_class"] == DEVICE_CLASS_CONNECTIVITY + assert entity.attributes["attribution"] == ATTRIBUTION + assert entity.attributes["target"] == MOCK_UPTIMEROBOT_MONITOR["url"] + + +async def test_unaviable_on_update_failure(hass: HomeAssistant) -> None: + """Test entity unaviable on update failure.""" + await setup_uptimerobot_integration(hass) + + entity = hass.states.get(UPTIMEROBOT_TEST_ENTITY) + assert entity.state == STATE_ON + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + side_effect=UptimeRobotAuthenticationException, + ): + async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) + await hass.async_block_till_done() + + entity = hass.states.get(UPTIMEROBOT_TEST_ENTITY) + assert entity.state == STATE_UNAVAILABLE diff --git a/tests/components/uptimerobot/test_config_flow.py b/tests/components/uptimerobot/test_config_flow.py index 967e1b499f5..966483970d0 100644 --- a/tests/components/uptimerobot/test_config_flow.py +++ b/tests/components/uptimerobot/test_config_flow.py @@ -1,15 +1,13 @@ """Test the Uptime Robot config flow.""" from unittest.mock import patch +import pytest from pytest import LogCaptureFixture -from pyuptimerobot import UptimeRobotApiResponse -from pyuptimerobot.exceptions import ( - UptimeRobotAuthenticationException, - UptimeRobotException, -) +from pyuptimerobot import UptimeRobotAuthenticationException, UptimeRobotException from homeassistant import config_entries, setup from homeassistant.components.uptimerobot.const import DOMAIN +from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -17,6 +15,15 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) +from .common import ( + MOCK_UPTIMEROBOT_ACCOUNT, + MOCK_UPTIMEROBOT_API_KEY, + MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA, + MOCK_UPTIMEROBOT_UNIQUE_ID, + MockApiResponseKey, + mock_uptimerobot_api_response, +) + from tests.common import MockConfigEntry @@ -31,82 +38,49 @@ async def test_form(hass: HomeAssistant) -> None: with patch( "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=UptimeRobotApiResponse.from_dict( - { - "stat": "ok", - "account": {"email": "test@test.test", "user_id": 1234567890}, - } - ), + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), ), patch( "homeassistant.components.uptimerobot.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"api_key": "1234"}, + {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, ) await hass.async_block_till_done() - assert result2["result"].unique_id == "1234567890" + assert result2["result"].unique_id == MOCK_UPTIMEROBOT_UNIQUE_ID assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "test@test.test" - assert result2["data"] == {"api_key": "1234"} + assert result2["title"] == MOCK_UPTIMEROBOT_ACCOUNT["email"] + assert result2["data"] == {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY} assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_cannot_connect(hass: HomeAssistant) -> None: - """Test we handle cannot connect error.""" +@pytest.mark.parametrize( + "exception,error_key", + [ + (Exception, "unknown"), + (UptimeRobotException, "cannot_connect"), + (UptimeRobotAuthenticationException, "invalid_api_key"), + ], +) +async def test_form_exception_thrown(hass: HomeAssistant, exception, error_key) -> None: + """Test that we handle exceptions.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( "pyuptimerobot.UptimeRobot.async_get_account_details", - side_effect=UptimeRobotException, + side_effect=exception, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"api_key": "1234"}, + {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, ) assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"]["base"] == "cannot_connect" - - -async def test_form_unexpected_error(hass: HomeAssistant) -> None: - """Test we handle unexpected error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "pyuptimerobot.UptimeRobot.async_get_account_details", - side_effect=Exception, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"api_key": "1234"}, - ) - - assert result2["errors"]["base"] == "unknown" - - -async def test_form_api_key_error(hass: HomeAssistant) -> None: - """Test we handle unexpected error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "pyuptimerobot.UptimeRobot.async_get_account_details", - side_effect=UptimeRobotAuthenticationException, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"api_key": "1234"}, - ) - - assert result2["errors"]["base"] == "invalid_api_key" + assert result2["errors"]["base"] == error_key async def test_form_api_error(hass: HomeAssistant, caplog: LogCaptureFixture) -> None: @@ -117,32 +91,24 @@ async def test_form_api_error(hass: HomeAssistant, caplog: LogCaptureFixture) -> with patch( "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=UptimeRobotApiResponse.from_dict( - { - "stat": "fail", - "error": {"message": "test error from API."}, - } - ), + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ERROR), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"api_key": "1234"}, + {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, ) assert result2["errors"]["base"] == "unknown" assert "test error from API." in caplog.text -async def test_flow_import(hass): +async def test_flow_import( + hass: HomeAssistant, +) -> None: """Test an import flow.""" with patch( "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=UptimeRobotApiResponse.from_dict( - { - "stat": "ok", - "account": {"email": "test@test.test", "user_id": 1234567890}, - } - ), + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), ), patch( "homeassistant.components.uptimerobot.async_setup_entry", return_value=True, @@ -150,22 +116,17 @@ async def test_flow_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data={"platform": DOMAIN, "api_key": "1234"}, + data={"platform": DOMAIN, CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, ) await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"] == {"api_key": "1234"} + assert result["data"] == {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY} with patch( "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=UptimeRobotApiResponse.from_dict( - { - "stat": "ok", - "account": {"email": "test@test.test", "user_id": 1234567890}, - } - ), + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), ), patch( "homeassistant.components.uptimerobot.async_setup_entry", return_value=True, @@ -173,7 +134,7 @@ async def test_flow_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data={"platform": DOMAIN, "api_key": "1234"}, + data={"platform": DOMAIN, CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, ) await hass.async_block_till_done() @@ -183,7 +144,9 @@ async def test_flow_import(hass): with patch( "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=UptimeRobotApiResponse.from_dict({"stat": "ok"}), + return_value=mock_uptimerobot_api_response( + key=MockApiResponseKey.ACCOUNT, data={} + ), ), patch( "homeassistant.components.uptimerobot.async_setup_entry", return_value=True, @@ -191,7 +154,7 @@ async def test_flow_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data={"platform": DOMAIN, "api_key": "12345"}, + data={"platform": DOMAIN, CONF_API_KEY: "12345"}, ) await hass.async_block_till_done() @@ -199,13 +162,11 @@ async def test_flow_import(hass): assert result["reason"] == "unknown" -async def test_user_unique_id_already_exists(hass): +async def test_user_unique_id_already_exists( + hass: HomeAssistant, +) -> None: """Test creating an entry where the unique_id already exists.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={"platform": DOMAIN, "api_key": "1234"}, - unique_id="1234567890", - ) + entry = MockConfigEntry(**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -216,19 +177,14 @@ async def test_user_unique_id_already_exists(hass): with patch( "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=UptimeRobotApiResponse.from_dict( - { - "stat": "ok", - "account": {"email": "test@test.test", "user_id": 1234567890}, - } - ), + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), ), patch( "homeassistant.components.uptimerobot.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"api_key": "12345"}, + {CONF_API_KEY: "12345"}, ) await hass.async_block_till_done() @@ -237,13 +193,11 @@ async def test_user_unique_id_already_exists(hass): assert result2["reason"] == "already_configured" -async def test_reauthentication(hass): +async def test_reauthentication( + hass: HomeAssistant, +) -> None: """Test Uptime Robot reauthentication.""" - old_entry = MockConfigEntry( - domain=DOMAIN, - data={"platform": DOMAIN, "api_key": "1234"}, - unique_id="1234567890", - ) + old_entry = MockConfigEntry(**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA) old_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -262,12 +216,7 @@ async def test_reauthentication(hass): with patch( "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=UptimeRobotApiResponse.from_dict( - { - "stat": "ok", - "account": {"email": "test@test.test", "user_id": 1234567890}, - } - ), + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), ), patch( "homeassistant.components.uptimerobot.async_setup_entry", return_value=True, @@ -275,7 +224,7 @@ async def test_reauthentication(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"api_key": "1234"}, + {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, ) await hass.async_block_till_done() @@ -283,13 +232,11 @@ async def test_reauthentication(hass): assert result2["reason"] == "reauth_successful" -async def test_reauthentication_failure(hass): +async def test_reauthentication_failure( + hass: HomeAssistant, +) -> None: """Test Uptime Robot reauthentication failure.""" - old_entry = MockConfigEntry( - domain=DOMAIN, - data={"platform": DOMAIN, "api_key": "1234"}, - unique_id="1234567890", - ) + old_entry = MockConfigEntry(**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA) old_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -308,12 +255,7 @@ async def test_reauthentication_failure(hass): with patch( "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=UptimeRobotApiResponse.from_dict( - { - "stat": "fail", - "error": {"message": "test error from API."}, - } - ), + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ERROR), ), patch( "homeassistant.components.uptimerobot.async_setup_entry", return_value=True, @@ -321,7 +263,7 @@ async def test_reauthentication_failure(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"api_key": "1234"}, + {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, ) await hass.async_block_till_done() @@ -330,11 +272,12 @@ async def test_reauthentication_failure(hass): assert result2["errors"]["base"] == "unknown" -async def test_reauthentication_failure_no_existing_entry(hass): +async def test_reauthentication_failure_no_existing_entry( + hass: HomeAssistant, +) -> None: """Test Uptime Robot reauthentication with no existing entry.""" old_entry = MockConfigEntry( - domain=DOMAIN, - data={"platform": DOMAIN, "api_key": "1234"}, + **{**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA, "unique_id": None} ) old_entry.add_to_hass(hass) @@ -354,12 +297,7 @@ async def test_reauthentication_failure_no_existing_entry(hass): with patch( "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=UptimeRobotApiResponse.from_dict( - { - "stat": "ok", - "account": {"email": "test@test.test", "user_id": 1234567890}, - } - ), + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), ), patch( "homeassistant.components.uptimerobot.async_setup_entry", return_value=True, @@ -367,7 +305,7 @@ async def test_reauthentication_failure_no_existing_entry(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"api_key": "1234"}, + {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, ) await hass.async_block_till_done() @@ -375,13 +313,11 @@ async def test_reauthentication_failure_no_existing_entry(hass): assert result2["reason"] == "reauth_failed_existing" -async def test_reauthentication_failure_account_not_matching(hass): +async def test_reauthentication_failure_account_not_matching( + hass: HomeAssistant, +) -> None: """Test Uptime Robot reauthentication failure when using another account.""" - old_entry = MockConfigEntry( - domain=DOMAIN, - data={"platform": DOMAIN, "api_key": "1234"}, - unique_id="1234567890", - ) + old_entry = MockConfigEntry(**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA) old_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -400,11 +336,9 @@ async def test_reauthentication_failure_account_not_matching(hass): with patch( "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=UptimeRobotApiResponse.from_dict( - { - "stat": "ok", - "account": {"email": "test@test.test", "user_id": 1234567891}, - } + return_value=mock_uptimerobot_api_response( + key=MockApiResponseKey.ACCOUNT, + data={**MOCK_UPTIMEROBOT_ACCOUNT, "user_id": 1234567891}, ), ), patch( "homeassistant.components.uptimerobot.async_setup_entry", @@ -413,7 +347,7 @@ async def test_reauthentication_failure_account_not_matching(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"api_key": "1234"}, + {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, ) await hass.async_block_till_done() diff --git a/tests/components/uptimerobot/test_init.py b/tests/components/uptimerobot/test_init.py index b4534af763a..756831e7615 100644 --- a/tests/components/uptimerobot/test_init.py +++ b/tests/components/uptimerobot/test_init.py @@ -1,16 +1,31 @@ """Test the Uptime Robot init.""" -import datetime from unittest.mock import patch from pytest import LogCaptureFixture -from pyuptimerobot import UptimeRobotApiResponse -from pyuptimerobot.exceptions import UptimeRobotAuthenticationException +from pyuptimerobot import UptimeRobotAuthenticationException, UptimeRobotException from homeassistant import config_entries -from homeassistant.components.uptimerobot.const import DOMAIN +from homeassistant.components.uptimerobot.const import ( + COORDINATOR_UPDATE_INTERVAL, + DOMAIN, +) +from homeassistant.const import STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import ( + async_entries_for_config_entry, + async_get_registry, +) from homeassistant.util import dt +from .common import ( + MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA, + MOCK_UPTIMEROBOT_MONITOR, + UPTIMEROBOT_TEST_ENTITY, + MockApiResponseKey, + mock_uptimerobot_api_response, + setup_uptimerobot_integration, +) + from tests.common import MockConfigEntry, async_fire_time_changed @@ -18,13 +33,7 @@ async def test_reauthentication_trigger_in_setup( hass: HomeAssistant, caplog: LogCaptureFixture ): """Test reauthentication trigger.""" - mock_config_entry = MockConfigEntry( - domain=DOMAIN, - title="test@test.test", - data={"platform": DOMAIN, "api_key": "1234"}, - unique_id="1234567890", - source=config_entries.SOURCE_USER, - ) + mock_config_entry = MockConfigEntry(**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA) mock_config_entry.add_to_hass(hass) with patch( @@ -57,46 +66,23 @@ async def test_reauthentication_trigger_after_setup( hass: HomeAssistant, caplog: LogCaptureFixture ): """Test reauthentication trigger.""" - mock_config_entry = MockConfigEntry( - domain=DOMAIN, - title="test@test.test", - data={"platform": DOMAIN, "api_key": "1234"}, - unique_id="1234567890", - source=config_entries.SOURCE_USER, - ) - mock_config_entry.add_to_hass(hass) + mock_config_entry = await setup_uptimerobot_integration(hass) - with patch( - "pyuptimerobot.UptimeRobot.async_get_monitors", - return_value=UptimeRobotApiResponse.from_dict( - { - "stat": "ok", - "monitors": [ - {"id": 1234, "friendly_name": "Test monitor", "status": 2} - ], - } - ), - ): - - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - binary_sensor = hass.states.get("binary_sensor.test_monitor") + binary_sensor = hass.states.get(UPTIMEROBOT_TEST_ENTITY) assert mock_config_entry.state == config_entries.ConfigEntryState.LOADED - assert binary_sensor.state == "on" + assert binary_sensor.state == STATE_ON with patch( "pyuptimerobot.UptimeRobot.async_get_monitors", side_effect=UptimeRobotAuthenticationException, ): - async_fire_time_changed(hass, dt.utcnow() + datetime.timedelta(seconds=10)) + async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) await hass.async_block_till_done() flows = hass.config_entries.flow.async_progress() - binary_sensor = hass.states.get("binary_sensor.test_monitor") + assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_UNAVAILABLE - assert binary_sensor.state == "unavailable" assert "Authentication failed while fetching uptimerobot data" in caplog.text assert len(flows) == 1 @@ -105,3 +91,96 @@ async def test_reauthentication_trigger_after_setup( assert flow["handler"] == DOMAIN assert flow["context"]["source"] == config_entries.SOURCE_REAUTH assert flow["context"]["entry_id"] == mock_config_entry.entry_id + + +async def test_integration_reload(hass: HomeAssistant): + """Test integration reload.""" + mock_entry = await setup_uptimerobot_integration(hass) + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + return_value=mock_uptimerobot_api_response(), + ): + assert await hass.config_entries.async_reload(mock_entry.entry_id) + async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) + await hass.async_block_till_done() + + entry = hass.config_entries.async_get_entry(mock_entry.entry_id) + assert entry.state == config_entries.ConfigEntryState.LOADED + assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_ON + + +async def test_update_errors(hass: HomeAssistant, caplog: LogCaptureFixture): + """Test errors during updates.""" + await setup_uptimerobot_integration(hass) + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + side_effect=UptimeRobotException, + ): + async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) + await hass.async_block_till_done() + assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_UNAVAILABLE + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + return_value=mock_uptimerobot_api_response(), + ): + async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) + await hass.async_block_till_done() + assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_ON + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ERROR), + ): + async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) + await hass.async_block_till_done() + assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_UNAVAILABLE + + assert "Error fetching uptimerobot data: test error from API" in caplog.text + + +async def test_device_management(hass: HomeAssistant): + """Test that we are adding and removing devices for monitors returned from the API.""" + mock_entry = await setup_uptimerobot_integration(hass) + dev_reg = await async_get_registry(hass) + + devices = async_entries_for_config_entry(dev_reg, mock_entry.entry_id) + assert len(devices) == 1 + + assert devices[0].identifiers == {(DOMAIN, "1234")} + + assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_ON + assert hass.states.get(f"{UPTIMEROBOT_TEST_ENTITY}_2") is None + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + return_value=mock_uptimerobot_api_response( + data=[MOCK_UPTIMEROBOT_MONITOR, {**MOCK_UPTIMEROBOT_MONITOR, "id": 12345}] + ), + ): + async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) + await hass.async_block_till_done() + + devices = async_entries_for_config_entry(dev_reg, mock_entry.entry_id) + assert len(devices) == 2 + assert devices[0].identifiers == {(DOMAIN, "1234")} + assert devices[1].identifiers == {(DOMAIN, "12345")} + + assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_ON + assert hass.states.get(f"{UPTIMEROBOT_TEST_ENTITY}_2").state == STATE_ON + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + return_value=mock_uptimerobot_api_response(), + ): + async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) + await hass.async_block_till_done() + + devices = async_entries_for_config_entry(dev_reg, mock_entry.entry_id) + assert len(devices) == 1 + assert devices[0].identifiers == {(DOMAIN, "1234")} + + assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_ON + assert hass.states.get(f"{UPTIMEROBOT_TEST_ENTITY}_2") is None From 7e211965e4b789cf71951b6ff1958c43b18a0f29 Mon Sep 17 00:00:00 2001 From: Dror Eiger <45061021+deiger@users.noreply.github.com> Date: Tue, 10 Aug 2021 17:31:55 +0300 Subject: [PATCH 125/355] Update the Qubino Flush Shutter fixture (#54387) --- tests/components/zwave_js/test_cover.py | 2 +- .../zwave_js/cover_qubino_shutter_state.json | 961 ++++++++++-------- 2 files changed, 549 insertions(+), 414 deletions(-) diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index 70ce2337abf..1afe7a114da 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -23,7 +23,7 @@ from homeassistant.const import ( WINDOW_COVER_ENTITY = "cover.zws_12" GDC_COVER_ENTITY = "cover.aeon_labs_garage_door_controller_gen5" BLIND_COVER_ENTITY = "cover.window_blind_controller" -SHUTTER_COVER_ENTITY = "cover.flush_shutter_dc" +SHUTTER_COVER_ENTITY = "cover.flush_shutter" AEOTEC_SHUTTER_COVER_ENTITY = "cover.nano_shutter_v_3" diff --git a/tests/fixtures/zwave_js/cover_qubino_shutter_state.json b/tests/fixtures/zwave_js/cover_qubino_shutter_state.json index 65725606e1c..bde7c90e1e4 100644 --- a/tests/fixtures/zwave_js/cover_qubino_shutter_state.json +++ b/tests/fixtures/zwave_js/cover_qubino_shutter_state.json @@ -1,48 +1,104 @@ { - "nodeId": 5, + "nodeId": 20, "index": 0, "installerIcon": 6656, "userIcon": 6656, "status": 4, "ready": true, - "deviceClass": { - "basic": { "key": 4, "label": "Routing Slave" }, - "generic": { "key": 17, "label": "Routing Slave" }, - "specific": { "key": 7, "label": "Routing Slave" }, - "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] - }, "isListening": true, - "isFrequentListening": false, "isRouting": true, - "maxBaudRate": 40000, "isSecure": false, - "version": 4, - "isBeaming": true, "manufacturerId": 345, - "productId": 83, + "productId": 82, "productType": 3, - "firmwareVersion": "7.2", + "firmwareVersion": "71.0", "zwavePlusVersion": 1, - "nodeType": 0, - "roleType": 5, "deviceConfig": { - "manufacturerId": 345, + "filename": "/data/db/devices/0x0159/zmnhcd_4.1.json", + "isEmbedded": true, "manufacturer": "Qubino", - "label": "ZMNHOD", - "description": "Flush Shutter DC", - "devices": [{ "productType": "0x0003", "productId": "0x0053" }], - "firmwareVersion": { "min": "0.0", "max": "255.255" }, - "paramInformation": { "_map": {} } + "manufacturerId": 345, + "label": "ZMNHCD", + "description": "Flush Shutter", + "devices": [ + { + "productType": 3, + "productId": 82 + } + ], + "firmwareVersion": { + "min": "4.1", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + } }, - "label": "ZMNHOD", - "neighbors": [1, 2], - "interviewAttempts": 1, + "label": "ZMNHCD", + "interviewAttempts": 0, "endpoints": [ - { "nodeId": 5, "index": 0, "installerIcon": 6656, "userIcon": 6656 } + { + "nodeId": 20, + "index": 0, + "installerIcon": 6656, + "userIcon": 6656, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 7, + "label": "Motor Control Class C" + }, + "mandatorySupportedCCs": [ + 32, + 38, + 37, + 114, + 134 + ], + "mandatoryControlledCCs": [] + } + } ], - "commandClasses": [], "values": [ + { + "endpoint": 0, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": [ + "transitionDuration" + ] + } + }, { "endpoint": 0, "commandClass": 38, @@ -54,10 +110,14 @@ "type": "number", "readable": true, "writeable": true, + "label": "Target value", + "valueChangeOptions": [ + "transitionDuration" + ], "min": 0, - "max": 99, - "label": "Target value" - } + "max": 99 + }, + "value": 99 }, { "endpoint": 0, @@ -84,11 +144,11 @@ "type": "number", "readable": true, "writeable": false, + "label": "Current value", "min": 0, - "max": 99, - "label": "Current value" + "max": 99 }, - "value": "unknown" + "value": 0 }, { "endpoint": 0, @@ -102,7 +162,9 @@ "readable": true, "writeable": true, "label": "Perform a level change (Up)", - "ccSpecific": { "switchType": 2 } + "ccSpecific": { + "switchType": 2 + } } }, { @@ -117,146 +179,9 @@ "readable": true, "writeable": true, "label": "Perform a level change (Down)", - "ccSpecific": { "switchType": 2 } - } - }, - { - "endpoint": 0, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value" - }, - "value": "unknown" - }, - { - "endpoint": 0, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value" - } - }, - { - "endpoint": 0, - "commandClass": 114, - "commandClassName": "Manufacturer Specific", - "property": "manufacturerId", - "propertyName": "manufacturerId", - "ccVersion": 2, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "min": 0, - "max": 65535, - "label": "Manufacturer ID" - }, - "value": 345 - }, - { - "endpoint": 0, - "commandClass": 114, - "commandClassName": "Manufacturer Specific", - "property": "productType", - "propertyName": "productType", - "ccVersion": 2, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "min": 0, - "max": 65535, - "label": "Product type" - }, - "value": 3 - }, - { - "endpoint": 0, - "commandClass": 114, - "commandClassName": "Manufacturer Specific", - "property": "productId", - "propertyName": "productId", - "ccVersion": 2, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "min": 0, - "max": 65535, - "label": "Product ID" - }, - "value": 83 - }, - { - "endpoint": 0, - "commandClass": 134, - "commandClassName": "Version", - "property": "libraryType", - "propertyName": "libraryType", - "ccVersion": 2, - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "Library type" - }, - "value": 3 - }, - { - "endpoint": 0, - "commandClass": 134, - "commandClassName": "Version", - "property": "protocolVersion", - "propertyName": "protocolVersion", - "ccVersion": 2, - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "Z-Wave protocol version" - }, - "value": "4.38" - }, - { - "endpoint": 0, - "commandClass": 134, - "commandClassName": "Version", - "property": "firmwareVersions", - "propertyName": "firmwareVersions", - "ccVersion": 2, - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "Z-Wave chip firmware versions" - }, - "value": ["7.2"] - }, - { - "endpoint": 0, - "commandClass": 134, - "commandClassName": "Version", - "property": "hardwareVersion", - "propertyName": "hardwareVersion", - "ccVersion": 2, - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "Z-Wave chip hardware version" + "ccSpecific": { + "switchType": 2 + } } }, { @@ -273,29 +198,14 @@ "readable": true, "writeable": false, "label": "Electric Consumed [kWh]", - "unit": "kWh", - "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 0 } + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 0 + }, + "unit": "kWh" }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 50, - "commandClassName": "Meter", - "property": "deltaTime", - "propertyKey": 65537, - "propertyName": "deltaTime", - "propertyKeyName": "Electric_kWh_Consumed", - "ccVersion": 4, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Electric Consumed [kWh] (prev. time delta)", - "unit": "s", - "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 0 } - }, - "value": 0 + "value": 7.9 }, { "endpoint": 0, @@ -311,27 +221,12 @@ "readable": true, "writeable": false, "label": "Electric Consumed [W]", - "unit": "W", - "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 2 } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 50, - "commandClassName": "Meter", - "property": "deltaTime", - "propertyKey": 66049, - "propertyName": "deltaTime", - "propertyKeyName": "Electric_W_Consumed", - "ccVersion": 4, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Electric Consumed [W] (prev. time delta)", - "unit": "s", - "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 2 } + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 2 + }, + "unit": "W" }, "value": 0 }, @@ -349,119 +244,31 @@ "label": "Reset accumulated values" } }, - { - "endpoint": 0, - "commandClass": 50, - "commandClassName": "Meter", - "property": "previousValue", - "propertyKey": 65537, - "propertyName": "previousValue", - "propertyKeyName": "Electric_kWh_Consumed", - "ccVersion": 4, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Electric Consumed [kWh] (prev. value)", - "unit": "kWh", - "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 0 } - } - }, - { - "endpoint": 0, - "commandClass": 50, - "commandClassName": "Meter", - "property": "previousValue", - "propertyKey": 66049, - "propertyName": "previousValue", - "propertyKeyName": "Electric_W_Consumed", - "ccVersion": 4, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Electric Consumed [W] (prev. value)", - "unit": "W", - "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 2 } - } - }, - { - "endpoint": 0, - "commandClass": 113, - "commandClassName": "Notification", - "property": "alarmType", - "propertyName": "alarmType", - "ccVersion": 5, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "min": 0, - "max": 255, - "label": "Alarm Type" - } - }, - { - "endpoint": 0, - "commandClass": 113, - "commandClassName": "Notification", - "property": "alarmLevel", - "propertyName": "alarmLevel", - "ccVersion": 5, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "min": 0, - "max": 255, - "label": "Alarm Level" - } - }, - { - "endpoint": 0, - "commandClass": 113, - "commandClassName": "Notification", - "property": "Power Management", - "propertyKey": "Over-load status", - "propertyName": "Power Management", - "propertyKeyName": "Over-load status", - "ccVersion": 5, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "min": 0, - "max": 255, - "label": "Over-load status", - "states": { "0": "idle", "8": "Over-load detected" }, - "ccSpecific": { "notificationType": 8 } - }, - "value": 0 - }, { "endpoint": 0, "commandClass": 112, "commandClassName": "Configuration", "property": 10, - "propertyName": "Activate/deactivate functions ALL ON / ALL OFF", + "propertyName": "ALL ON/ALL OFF", "ccVersion": 1, "metadata": { "type": "number", "readable": true, "writeable": true, - "valueSize": 2, - "min": 0, - "max": 65535, + "description": "Responds to commands ALL ON / ALL OFF from Main Controller", + "label": "ALL ON/ALL OFF", "default": 255, - "format": 1, - "allowManualEntry": false, + "min": 0, + "max": 255, "states": { - "0": "ALL ON is not active, ALL OFF is not active", + "0": "ALL ON is not active ALL OFF is not active", "1": "ALL ON is not active ALL OFF active", "2": "ALL ON is not active ALL OFF is not active", "255": "ALL ON active, ALL OFF active" }, - "label": "Activate/deactivate functions ALL ON / ALL OFF", + "valueSize": 2, + "format": 0, + "allowManualEntry": false, "isFromConfig": true }, "value": 255 @@ -471,19 +278,20 @@ "commandClass": 112, "commandClassName": "Configuration", "property": 40, - "propertyName": "Power report (Watts) on power change for Q1 or Q2", + "propertyName": "Power reporting in watts on power change", "ccVersion": 1, "metadata": { "type": "number", "readable": true, "writeable": true, - "valueSize": 1, + "description": "Power consumption change threshold for sending updates", + "label": "Power reporting in watts on power change", + "default": 1, "min": 0, "max": 100, - "default": 1, + "valueSize": 1, "format": 0, "allowManualEntry": true, - "label": "Power report (Watts) on power change for Q1 or Q2", "isFromConfig": true }, "value": 10 @@ -493,19 +301,20 @@ "commandClass": 112, "commandClassName": "Configuration", "property": 42, - "propertyName": "Power report (Watts) by time interval for Q1 or Q2", + "propertyName": "Power reporting in Watts by time interval", "ccVersion": 1, "metadata": { "type": "number", "readable": true, "writeable": true, - "valueSize": 2, + "label": "Power reporting in Watts by time interval", + "default": 300, "min": 0, "max": 32767, - "default": 300, + "unit": "seconds", + "valueSize": 2, "format": 0, "allowManualEntry": true, - "label": "Power report (Watts) by time interval for Q1 or Q2", "isFromConfig": true }, "value": 0 @@ -521,17 +330,18 @@ "type": "number", "readable": true, "writeable": true, - "valueSize": 1, + "description": "Operation Mode (Shutter or Venetian)", + "label": "Operating modes", + "default": 0, "min": 0, "max": 255, - "default": 0, - "format": 1, - "allowManualEntry": false, "states": { - "0": "Shutter mode.", + "0": "Shutter mode", "1": "Venetian mode (up/down and slate rotation)" }, - "label": "Operating modes", + "valueSize": 1, + "format": 1, + "allowManualEntry": false, "isFromConfig": true }, "value": 0 @@ -547,16 +357,18 @@ "type": "number", "readable": true, "writeable": true, - "valueSize": 2, + "description": "Slat full turn time in tenths of a second.", + "label": "Slats tilting full turn time", + "default": 150, "min": 0, "max": 32767, - "default": 150, + "unit": "tenths of a second", + "valueSize": 2, "format": 0, "allowManualEntry": true, - "label": "Slats tilting full turn time", "isFromConfig": true }, - "value": 630 + "value": 150 }, { "endpoint": 0, @@ -569,43 +381,22 @@ "type": "number", "readable": true, "writeable": true, - "valueSize": 1, - "min": 0, - "max": 255, - "default": 1, - "format": 1, - "allowManualEntry": false, - "states": { - "0": "Return to previous position only with Z-wave", - "1": "Return to previous position with Z-wave or button" - }, + "description": "Slats position after up/down movement.", "label": "Slats position", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Previous position for Z-wave control only", + "1": "Return to previous position in all cases" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, "isFromConfig": true }, "value": 1 }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 74, - "propertyName": "Motor moving up/down time", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "valueSize": 2, - "min": 0, - "max": 32767, - "default": 0, - "format": 0, - "allowManualEntry": true, - "label": "Motor moving up/down time", - "isFromConfig": true - }, - "value": 0 - }, { "endpoint": 0, "commandClass": 112, @@ -617,36 +408,41 @@ "type": "number", "readable": true, "writeable": true, - "valueSize": 1, + "description": "Power threshold to be interpreted when motor reach the limit switch", + "label": "Motor operation detection", + "default": 10, "min": 0, - "max": 100, - "default": 6, + "max": 127, + "valueSize": 1, "format": 0, "allowManualEntry": true, - "label": "Motor operation detection", "isFromConfig": true }, - "value": 10 + "value": 30 }, { "endpoint": 0, "commandClass": 112, "commandClassName": "Configuration", "property": 78, - "propertyName": "Forced Shutter DC calibration", + "propertyName": "Forced Shutter calibration", "ccVersion": 1, "metadata": { "type": "number", "readable": true, "writeable": true, - "valueSize": 1, - "min": 0, - "max": 255, + "description": "Enters calibration mode if set to 1", + "label": "Forced Shutter calibration", "default": 0, - "format": 1, + "min": 0, + "max": 1, + "states": { + "0": "Default", + "1": "Start Calibration Process" + }, + "valueSize": 1, + "format": 0, "allowManualEntry": false, - "states": { "0": "Default", "1": "Start calibration process." }, - "label": "Forced Shutter DC calibration", "isFromConfig": true }, "value": 0 @@ -662,57 +458,38 @@ "type": "number", "readable": true, "writeable": true, - "valueSize": 1, - "min": 3, - "max": 50, - "default": 8, - "format": 0, - "allowManualEntry": true, + "description": "Time delay for detecting motor errors", "label": "Power consumption max delay time", - "isFromConfig": true - }, - "value": 8 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 86, - "propertyName": "Power consumption at limit switch delay time", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "valueSize": 1, - "min": 3, - "max": 50, "default": 8, + "min": 0, + "max": 50, + "valueSize": 1, "format": 0, "allowManualEntry": true, - "label": "Power consumption at limit switch delay time", "isFromConfig": true }, - "value": 8 + "value": 30 }, { "endpoint": 0, "commandClass": 112, "commandClassName": "Configuration", "property": 90, - "propertyName": "Time delay for next motor movement", + "propertyName": "Relay delay time", "ccVersion": 1, "metadata": { "type": "number", "readable": true, "writeable": true, - "valueSize": 1, + "description": "Defines the minimum time delay between next motor movement", + "label": "Relay delay time", + "default": 5, "min": 1, "max": 30, - "default": 5, + "unit": "milliseconds", + "valueSize": 1, "format": 0, "allowManualEntry": true, - "label": "Time delay for next motor movement", "isFromConfig": true }, "value": 5 @@ -728,13 +505,14 @@ "type": "number", "readable": true, "writeable": true, - "valueSize": 2, + "description": "Adds or removes an offset from the measured temperature.", + "label": "Temperature sensor offset settings", + "default": 32536, "min": 1, "max": 32536, - "default": 32536, + "valueSize": 2, "format": 0, "allowManualEntry": true, - "label": "Temperature sensor offset settings", "isFromConfig": true }, "value": 32536 @@ -750,16 +528,373 @@ "type": "number", "readable": true, "writeable": true, - "valueSize": 1, + "description": "Threshold for sending temperature change reports", + "label": "Digital temperature sensor reporting", + "default": 5, "min": 0, "max": 127, - "default": 5, + "valueSize": 1, "format": 0, "allowManualEntry": true, - "label": "Digital temperature sensor reporting", "isFromConfig": true }, "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 74, + "propertyName": "Motor moving up/down time", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Shutter motor moving time of complete opening or complete closing", + "label": "Motor moving up/down time", + "default": 0, + "min": 0, + "max": 32767, + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 80, + "propertyName": "Reporting to Controller", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Defines if reporting regarding power level, etc is reported to controller.", + "label": "Reporting to Controller", + "default": 1, + "min": 0, + "max": 255, + "states": { + "0": "Reporting to Controller Disabled", + "1": "Reporting to Controller Enabled" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 86, + "propertyName": "Power consumption at limit switch delay time", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the time delay for detecting limit switches", + "label": "Power consumption at limit switch delay time", + "default": 8, + "min": 3, + "max": 50, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "unknown", + "propertyName": "Power Management", + "propertyKeyName": "unknown", + "ccVersion": 5, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 254 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Over-load status", + "propertyName": "Power Management", + "propertyKeyName": "Over-load status", + "ccVersion": 5, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Over-load status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "8": "Over-load detected" + } + } + }, + { + "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": 345 + }, + { + "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": 3 + }, + { + "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": 82 + }, + { + "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.38" + }, + { + "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": [ + "71.0", + "71.0" + ] + }, + { + "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": 2 } - ] + ], + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [ + 40000, + 100000 + ], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 7, + "label": "Motor Control Class C" + }, + "mandatorySupportedCCs": [ + 32, + 38, + 37, + 114, + 134 + ], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 3, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 4, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 2, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 5, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + } + ], + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0159:0x0003:0x0052:71.0", + "statistics": { + "commandsTX": 17, + "commandsRX": 57, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 + } } From f5901265dc9518dabb4800f80c7b675c34da82fb Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 10 Aug 2021 16:47:52 +0200 Subject: [PATCH 126/355] Use EntityDescription - ios (#54359) * Use EntityDescription - ios * Make attribute static * Update homeassistant/components/ios/sensor.py Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/ios/sensor.py | 79 +++++++++++--------------- 1 file changed, 33 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/ios/sensor.py b/homeassistant/components/ios/sensor.py index c1442f0de9f..d3b006f9078 100644 --- a/homeassistant/components/ios/sensor.py +++ b/homeassistant/components/ios/sensor.py @@ -1,6 +1,8 @@ """Support for Home Assistant iOS app sensors.""" +from __future__ import annotations + from homeassistant.components import ios -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import PERCENTAGE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -8,10 +10,17 @@ from homeassistant.helpers.icon import icon_for_battery_level from .const import DOMAIN -SENSOR_TYPES = { - "level": ["Battery Level", PERCENTAGE], - "state": ["Battery State", None], -} +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="level", + name="Battery Level", + unit_of_measurement=PERCENTAGE, + ), + SensorEntityDescription( + key="state", + name="Battery State", + ), +) DEFAULT_ICON_LEVEL = "mdi:battery" DEFAULT_ICON_STATE = "mdi:power-plug" @@ -24,25 +33,30 @@ def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_entry(hass, config_entry, async_add_entities): """Set up iOS from a config entry.""" - dev = [] - for device_name, device in ios.devices(hass).items(): - for sensor_type in ("level", "state"): - dev.append(IOSSensor(sensor_type, device_name, device)) + entities = [ + IOSSensor(device_name, device, description) + for device_name, device in ios.devices(hass).items() + for description in SENSOR_TYPES + ] - async_add_entities(dev, True) + async_add_entities(entities, True) class IOSSensor(SensorEntity): """Representation of an iOS sensor.""" - def __init__(self, sensor_type, device_name, device): + _attr_should_poll = False + + def __init__(self, device_name, device, description: SensorEntityDescription): """Initialize the sensor.""" - self._device_name = device_name - self._name = f"{device_name} {SENSOR_TYPES[sensor_type][0]}" + self.entity_description = description self._device = device - self.type = sensor_type - self._state = None - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + + device_name = device[ios.ATTR_DEVICE][ios.ATTR_DEVICE_NAME] + self._attr_name = f"{device_name} {description.key}" + + device_id = device[ios.ATTR_DEVICE_ID] + self._attr_unique_id = f"{description.key}_{device_id}" @property def device_info(self): @@ -60,33 +74,6 @@ class IOSSensor(SensorEntity): "sw_version": self._device[ios.ATTR_DEVICE][ios.ATTR_DEVICE_SYSTEM_VERSION], } - @property - def name(self): - """Return the name of the iOS sensor.""" - device_name = self._device[ios.ATTR_DEVICE][ios.ATTR_DEVICE_NAME] - return f"{device_name} {SENSOR_TYPES[self.type][0]}" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unique_id(self): - """Return the unique ID of this sensor.""" - device_id = self._device[ios.ATTR_DEVICE_ID] - return f"{self.type}_{device_id}" - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def unit_of_measurement(self): - """Return the unit of measurement this sensor expresses itself in.""" - return self._unit_of_measurement - @property def extra_state_attributes(self): """Return the device state attributes.""" @@ -119,7 +106,7 @@ class IOSSensor(SensorEntity): charging = False icon_state = f"{DEFAULT_ICON_LEVEL}-unknown" - if self.type == "state": + if self.entity_description.key == "state": return icon_state return icon_for_battery_level(battery_level=battery_level, charging=charging) @@ -127,12 +114,12 @@ class IOSSensor(SensorEntity): def _update(self, device): """Get the latest state of the sensor.""" self._device = device - self._state = self._device[ios.ATTR_BATTERY][self.type] + self._attr_state = self._device[ios.ATTR_BATTERY][self.entity_description.key] self.async_write_ha_state() async def async_added_to_hass(self) -> None: """Added to hass so need to register to dispatch.""" - self._state = self._device[ios.ATTR_BATTERY][self.type] + self._attr_state = self._device[ios.ATTR_BATTERY][self.entity_description.key] device_id = self._device[ios.ATTR_DEVICE_ID] self.async_on_remove( async_dispatcher_connect(self.hass, f"{DOMAIN}.{device_id}", self._update) From c0a7fca6281267326d3d8d4db416e53601be697c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 10 Aug 2021 18:57:25 +0200 Subject: [PATCH 127/355] Fix pi_hole sensor icon (#54403) --- homeassistant/components/pi_hole/__init__.py | 7 ++--- homeassistant/components/pi_hole/const.py | 28 +++++++++++++------- homeassistant/components/pi_hole/sensor.py | 8 ++++-- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index ab9191b0f4a..ddd4f77fa36 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -161,6 +161,8 @@ def _async_platforms(entry: ConfigEntry) -> list[str]: class PiHoleEntity(CoordinatorEntity): """Representation of a Pi-hole entity.""" + _attr_icon: str = "mdi:pi-hole" + def __init__( self, api: Hole, @@ -174,11 +176,6 @@ class PiHoleEntity(CoordinatorEntity): self._name = name self._server_unique_id = server_unique_id - @property - def icon(self) -> str: - """Icon to use in the frontend, if any.""" - return "mdi:pi-hole" - @property def device_info(self) -> DeviceInfo: """Return the device information of the entity.""" diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index 40a3a16de3a..52c638864a5 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -1,6 +1,7 @@ """Constants for the pi_hole integration.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta from homeassistant.components.sensor import SensorEntityDescription @@ -29,56 +30,63 @@ DATA_KEY_API = "api" DATA_KEY_COORDINATOR = "coordinator" -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( +@dataclass +class PiHoleSensorEntityDescription(SensorEntityDescription): + """Describes PiHole sensor entity.""" + + icon: str = "mdi:pi-hole" + + +SENSOR_TYPES: tuple[PiHoleSensorEntityDescription, ...] = ( + PiHoleSensorEntityDescription( key="ads_blocked_today", name="Ads Blocked Today", unit_of_measurement="ads", icon="mdi:close-octagon-outline", ), - SensorEntityDescription( + PiHoleSensorEntityDescription( key="ads_percentage_today", name="Ads Percentage Blocked Today", unit_of_measurement=PERCENTAGE, icon="mdi:close-octagon-outline", ), - SensorEntityDescription( + PiHoleSensorEntityDescription( key="clients_ever_seen", name="Seen Clients", unit_of_measurement="clients", icon="mdi:account-outline", ), - SensorEntityDescription( + PiHoleSensorEntityDescription( key="dns_queries_today", name="DNS Queries Today", unit_of_measurement="queries", icon="mdi:comment-question-outline", ), - SensorEntityDescription( + PiHoleSensorEntityDescription( key="domains_being_blocked", name="Domains Blocked", unit_of_measurement="domains", icon="mdi:block-helper", ), - SensorEntityDescription( + PiHoleSensorEntityDescription( key="queries_cached", name="DNS Queries Cached", unit_of_measurement="queries", icon="mdi:comment-question-outline", ), - SensorEntityDescription( + PiHoleSensorEntityDescription( key="queries_forwarded", name="DNS Queries Forwarded", unit_of_measurement="queries", icon="mdi:comment-question-outline", ), - SensorEntityDescription( + PiHoleSensorEntityDescription( key="unique_clients", name="DNS Unique Clients", unit_of_measurement="clients", icon="mdi:account-outline", ), - SensorEntityDescription( + PiHoleSensorEntityDescription( key="unique_domains", name="DNS Unique Domains", unit_of_measurement="domains", diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index 38b0b192e14..242f7a3a742 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -5,7 +5,7 @@ from typing import Any from hole import Hole -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant @@ -19,6 +19,7 @@ from .const import ( DATA_KEY_COORDINATOR, DOMAIN as PIHOLE_DOMAIN, SENSOR_TYPES, + PiHoleSensorEntityDescription, ) @@ -44,13 +45,15 @@ async def async_setup_entry( class PiHoleSensor(PiHoleEntity, SensorEntity): """Representation of a Pi-hole sensor.""" + entity_description: PiHoleSensorEntityDescription + def __init__( self, api: Hole, coordinator: DataUpdateCoordinator, name: str, server_unique_id: str, - description: SensorEntityDescription, + description: PiHoleSensorEntityDescription, ) -> None: """Initialize a Pi-hole sensor.""" super().__init__(api, coordinator, name, server_unique_id) @@ -58,6 +61,7 @@ class PiHoleSensor(PiHoleEntity, SensorEntity): self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{self._server_unique_id}/{description.name}" + self._attr_icon = description.icon # Necessary to overwrite inherited value @property def state(self) -> Any: From 1eeb12ba1c9b24ca6fe489dc0ded37a6c4f73351 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 10 Aug 2021 12:57:39 -0500 Subject: [PATCH 128/355] Support unloading/reloading Sonos (#54418) --- homeassistant/components/sonos/__init__.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index f0219ea8cf0..45f5cf9276c 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -138,6 +138,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a Sonos config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + await hass.data[DATA_SONOS_DISCOVERY_MANAGER].async_shutdown() + hass.data.pop(DATA_SONOS) + hass.data.pop(DATA_SONOS_DISCOVERY_MANAGER) + return unload_ok + + class SonosDiscoveryManager: """Manage sonos discovery.""" @@ -151,6 +160,11 @@ class SonosDiscoveryManager: self.hosts = hosts self.discovery_lock = asyncio.Lock() + async def async_shutdown(self): + """Stop all running tasks.""" + await self._async_stop_event_listener() + self._stop_manual_heartbeat() + def _create_soco(self, ip_address: str, source: SoCoCreationSource) -> SoCo | None: """Create a soco instance and return if successful.""" if ip_address in self.data.discovery_ignored: @@ -171,7 +185,7 @@ class SonosDiscoveryManager: ) return None - async def _async_stop_event_listener(self, event: Event) -> None: + async def _async_stop_event_listener(self, event: Event | None = None) -> None: await asyncio.gather( *(speaker.async_unsubscribe() for speaker in self.data.discovered.values()), return_exceptions=True, @@ -179,7 +193,7 @@ class SonosDiscoveryManager: if events_asyncio.event_listener: await events_asyncio.event_listener.async_stop() - def _stop_manual_heartbeat(self, event: Event) -> None: + def _stop_manual_heartbeat(self, event: Event | None = None) -> None: if self.data.hosts_heartbeat: self.data.hosts_heartbeat() self.data.hosts_heartbeat = None From ba6bdff04e6799e065da453fce33a06f24d40074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 10 Aug 2021 20:14:10 +0200 Subject: [PATCH 129/355] Re-add Tibber notify service name (#54401) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tibber/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index a18bb855f8f..da94df55c88 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -5,7 +5,7 @@ import logging import aiohttp import tibber -from homeassistant.const import CONF_ACCESS_TOKEN, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -62,7 +62,7 @@ async def async_setup_entry(hass, entry): # have to use discovery to load platform. hass.async_create_task( discovery.async_load_platform( - hass, "notify", DOMAIN, {}, hass.data[DATA_HASS_CONFIG] + hass, "notify", DOMAIN, {CONF_NAME: DOMAIN}, hass.data[DATA_HASS_CONFIG] ) ) return True From 2265fd1f81efa089b2db772ebad4a7fdc7108f3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 10 Aug 2021 20:26:48 +0200 Subject: [PATCH 130/355] Mark Uptime Robot as a platinum quality integration (#54408) --- homeassistant/components/uptimerobot/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/uptimerobot/manifest.json b/homeassistant/components/uptimerobot/manifest.json index 22d9a6d9477..279bf6eb43e 100644 --- a/homeassistant/components/uptimerobot/manifest.json +++ b/homeassistant/components/uptimerobot/manifest.json @@ -8,6 +8,7 @@ "codeowners": [ "@ludeeus" ], + "quality_scale": "platinum", "iot_class": "cloud_polling", "config_flow": true } \ No newline at end of file From ed0fd0074670ed56d056f139a9f934b3f89d9487 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 10 Aug 2021 11:30:02 -0700 Subject: [PATCH 131/355] Bump hass_nabucasa to 0.46.0 (#54421) --- homeassistant/components/cloud/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/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index abf73c1d54b..129b9f83819 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.45.1"], + "requirements": ["hass-nabucasa==0.46.0"], "dependencies": ["http", "webhook"], "after_dependencies": ["google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d03c4b7c4f7..323b1c86034 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ cryptography==3.3.2 defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 -hass-nabucasa==0.45.1 +hass-nabucasa==0.46.0 home-assistant-frontend==20210809.0 httpx==0.18.2 ifaddr==0.1.7 diff --git a/requirements_all.txt b/requirements_all.txt index 7c4001c4c5a..813f40acf54 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -753,7 +753,7 @@ habitipy==0.2.0 hangups==0.4.14 # homeassistant.components.cloud -hass-nabucasa==0.45.1 +hass-nabucasa==0.46.0 # homeassistant.components.splunk hass_splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f0b541ad31..4bc6cb76018 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -431,7 +431,7 @@ habitipy==0.2.0 hangups==0.4.14 # homeassistant.components.cloud -hass-nabucasa==0.45.1 +hass-nabucasa==0.46.0 # homeassistant.components.tasmota hatasmota==0.2.20 From 08a30ed5100579dea913ece3ff6ad6c96bee4bbe Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 10 Aug 2021 21:14:09 +0200 Subject: [PATCH 132/355] Add myself as codeowner to tradfri (IKEA stuff) (#54415) --- CODEOWNERS | 1 + homeassistant/components/tradfri/manifest.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index d2e756c0d0d..a7aea24c4f0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -527,6 +527,7 @@ homeassistant/components/tplink/* @rytilahti @thegardenmonkey homeassistant/components/traccar/* @ludeeus homeassistant/components/trace/* @home-assistant/core homeassistant/components/tractive/* @Danielhiversen @zhulik +homeassistant/components/tradfri/* @janiversen homeassistant/components/trafikverket_train/* @endor-force homeassistant/components/trafikverket_weatherstation/* @endor-force homeassistant/components/transmission/* @engrbm87 @JPHutchins diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index 3e13cdc015a..7ffad04074d 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -7,6 +7,6 @@ "homekit": { "models": ["TRADFRI"] }, - "codeowners": [], + "codeowners": ["@janiversen"], "iot_class": "local_polling" } From ac29571db331dbd08204398c44182e236f769926 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 10 Aug 2021 21:14:31 +0200 Subject: [PATCH 133/355] Refactor pi_hole icon usage (#54420) --- homeassistant/components/pi_hole/__init__.py | 2 -- homeassistant/components/pi_hole/binary_sensor.py | 2 ++ homeassistant/components/pi_hole/sensor.py | 1 - homeassistant/components/pi_hole/switch.py | 7 ++----- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index ddd4f77fa36..5c679a4839a 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -161,8 +161,6 @@ def _async_platforms(entry: ConfigEntry) -> list[str]: class PiHoleEntity(CoordinatorEntity): """Representation of a Pi-hole entity.""" - _attr_icon: str = "mdi:pi-hole" - def __init__( self, api: Hole, diff --git a/homeassistant/components/pi_hole/binary_sensor.py b/homeassistant/components/pi_hole/binary_sensor.py index 3c322d324d3..5758c0e4145 100644 --- a/homeassistant/components/pi_hole/binary_sensor.py +++ b/homeassistant/components/pi_hole/binary_sensor.py @@ -29,6 +29,8 @@ async def async_setup_entry( class PiHoleBinarySensor(PiHoleEntity, BinarySensorEntity): """Representation of a Pi-hole binary sensor.""" + _attr_icon = "mdi:pi-hole" + @property def name(self) -> str: """Return the name of the sensor.""" diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index 242f7a3a742..14aed86a479 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -61,7 +61,6 @@ class PiHoleSensor(PiHoleEntity, SensorEntity): self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{self._server_unique_id}/{description.name}" - self._attr_icon = description.icon # Necessary to overwrite inherited value @property def state(self) -> Any: diff --git a/homeassistant/components/pi_hole/switch.py b/homeassistant/components/pi_hole/switch.py index b0c4b09c2e7..dc699beb26b 100644 --- a/homeassistant/components/pi_hole/switch.py +++ b/homeassistant/components/pi_hole/switch.py @@ -58,6 +58,8 @@ async def async_setup_entry( class PiHoleSwitch(PiHoleEntity, SwitchEntity): """Representation of a Pi-hole switch.""" + _attr_icon = "mdi:pi-hole" + @property def name(self) -> str: """Return the name of the switch.""" @@ -68,11 +70,6 @@ class PiHoleSwitch(PiHoleEntity, SwitchEntity): """Return the unique id of the switch.""" return f"{self._server_unique_id}/Switch" - @property - def icon(self) -> str: - """Icon to use in the frontend, if any.""" - return "mdi:pi-hole" - @property def is_on(self) -> bool: """Return if the service is on.""" From 4bde4504ec087625335d0023114fbfe7356cb008 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 10 Aug 2021 14:21:34 -0500 Subject: [PATCH 134/355] Add api to device_automation to return all matching devices (#53361) --- .../components/device_automation/__init__.py | 110 ++++++++++++------ tests/common.py | 13 ++- .../components/device_automation/test_init.py | 71 +++++++++++ tests/components/remote/test_device_action.py | 4 +- tests/components/switch/test_device_action.py | 4 +- tests/components/zha/test_device_action.py | 5 +- 6 files changed, 159 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 93b0b9a4a9d..945774da0b4 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import MutableMapping +from collections.abc import Iterable, Mapping from functools import wraps from types import ModuleType from typing import Any @@ -13,9 +13,12 @@ import voluptuous_serialize from homeassistant.components import websocket_api from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity_registry import async_entries_for_device -from homeassistant.loader import IntegrationNotFound +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_registry as er, +) +from homeassistant.loader import IntegrationNotFound, bind_hass from homeassistant.requirements import async_get_integration_with_requirements from .exceptions import DeviceNotFound, InvalidDeviceAutomationConfig @@ -49,6 +52,16 @@ TYPES = { } +@bind_hass +async def async_get_device_automations( + hass: HomeAssistant, + automation_type: str, + device_ids: Iterable[str] | None = None, +) -> Mapping[str, Any]: + """Return all the device automations for a type optionally limited to specific device ids.""" + return await _async_get_device_automations(hass, automation_type, device_ids) + + async def async_setup(hass, config): """Set up device automation.""" hass.components.websocket_api.async_register_command( @@ -96,7 +109,7 @@ async def async_get_device_automation_platform( async def _async_get_device_automations_from_domain( - hass, domain, automation_type, device_id + hass, domain, automation_type, device_ids, return_exceptions ): """List device automations.""" try: @@ -104,48 +117,67 @@ async def _async_get_device_automations_from_domain( hass, domain, automation_type ) except InvalidDeviceAutomationConfig: - return None + return {} function_name = TYPES[automation_type][1] - return await getattr(platform, function_name)(hass, device_id) - - -async def _async_get_device_automations(hass, automation_type, device_id): - """List device automations.""" - device_registry, entity_registry = await asyncio.gather( - hass.helpers.device_registry.async_get_registry(), - hass.helpers.entity_registry.async_get_registry(), + return await asyncio.gather( + *( + getattr(platform, function_name)(hass, device_id) + for device_id in device_ids + ), + return_exceptions=return_exceptions, ) - domains = set() - automations: list[MutableMapping[str, Any]] = [] - device = device_registry.async_get(device_id) - if device is None: - raise DeviceNotFound +async def _async_get_device_automations( + hass: HomeAssistant, automation_type: str, device_ids: Iterable[str] | None +) -> Mapping[str, list[dict[str, Any]]]: + """List device automations.""" + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + domain_devices: dict[str, set[str]] = {} + device_entities_domains: dict[str, set[str]] = {} + match_device_ids = set(device_ids or device_registry.devices) + combined_results: dict[str, list[dict[str, Any]]] = {} - for entry_id in device.config_entries: - config_entry = hass.config_entries.async_get_entry(entry_id) - domains.add(config_entry.domain) + for entry in entity_registry.entities.values(): + if not entry.disabled_by and entry.device_id in match_device_ids: + device_entities_domains.setdefault(entry.device_id, set()).add(entry.domain) - entity_entries = async_entries_for_device(entity_registry, device_id) - for entity_entry in entity_entries: - domains.add(entity_entry.domain) + for device_id in match_device_ids: + combined_results[device_id] = [] + device = device_registry.async_get(device_id) + if device is None: + raise DeviceNotFound + for entry_id in device.config_entries: + if config_entry := hass.config_entries.async_get_entry(entry_id): + domain_devices.setdefault(config_entry.domain, set()).add(device_id) + for domain in device_entities_domains.get(device_id, []): + domain_devices.setdefault(domain, set()).add(device_id) - device_automations = await asyncio.gather( + # If specific device ids were requested, we allow + # InvalidDeviceAutomationConfig to be thrown, otherwise we skip + # devices that do not have valid triggers + return_exceptions = not bool(device_ids) + + for domain_results in await asyncio.gather( *( _async_get_device_automations_from_domain( - hass, domain, automation_type, device_id + hass, domain, automation_type, domain_device_ids, return_exceptions ) - for domain in domains + for domain, domain_device_ids in domain_devices.items() ) - ) - for device_automation in device_automations: - if device_automation is not None: - automations.extend(device_automation) + ): + for device_results in domain_results: + if device_results is None or isinstance( + device_results, InvalidDeviceAutomationConfig + ): + continue + for automation in device_results: + combined_results[automation["device_id"]].append(automation) - return automations + return combined_results async def _async_get_device_automation_capabilities(hass, automation_type, automation): @@ -207,7 +239,9 @@ 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) + actions = (await _async_get_device_automations(hass, "action", [device_id])).get( + device_id + ) connection.send_result(msg["id"], actions) @@ -222,7 +256,9 @@ async def websocket_device_automation_list_actions(hass, connection, msg): 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) + conditions = ( + await _async_get_device_automations(hass, "condition", [device_id]) + ).get(device_id) connection.send_result(msg["id"], conditions) @@ -237,7 +273,9 @@ 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) + triggers = (await _async_get_device_automations(hass, "trigger", [device_id])).get( + device_id + ) connection.send_result(msg["id"], triggers) diff --git a/tests/common.py b/tests/common.py index 5de58a08472..3d5e28be514 100644 --- a/tests/common.py +++ b/tests/common.py @@ -29,10 +29,9 @@ from homeassistant.auth import ( providers as auth_providers, ) from homeassistant.auth.permissions import system_policies -from homeassistant.components import recorder +from homeassistant.components import device_automation, recorder from homeassistant.components.device_automation import ( # noqa: F401 _async_get_device_automation_capabilities as async_get_device_automation_capabilities, - _async_get_device_automations as async_get_device_automations, ) from homeassistant.components.mqtt.models import ReceiveMessage from homeassistant.config import async_process_component_config @@ -69,6 +68,16 @@ CLIENT_ID = "https://example.com/app" CLIENT_REDIRECT_URI = "https://example.com/app/callback" +async def async_get_device_automations( + hass: HomeAssistant, automation_type: str, device_id: str +) -> Any: + """Get a device automation for a single device id.""" + automations = await device_automation.async_get_device_automations( + hass, automation_type, [device_id] + ) + return automations.get(device_id) + + def threadsafe_callback_factory(func): """Create threadsafe functions out of callbacks. diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 160e6354b8b..13190ed4b32 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -1,6 +1,7 @@ """The test for light device automation.""" import pytest +from homeassistant.components import device_automation import homeassistant.components.automation as automation from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON @@ -372,6 +373,76 @@ async def test_websocket_get_no_condition_capabilities( assert capabilities == expected_capabilities +async def test_async_get_device_automations_single_device_trigger( + hass, device_reg, entity_reg +): + """Test we get can fetch the triggers for a device id.""" + await async_setup_component(hass, "device_automation", {}) + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + 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", [device_entry.id] + ) + assert device_entry.id in result + assert len(result[device_entry.id]) == 2 + + +async def test_async_get_device_automations_all_devices_trigger( + hass, device_reg, entity_reg +): + """Test we get can fetch all the triggers when no device id is passed.""" + await async_setup_component(hass, "device_automation", {}) + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + 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") + assert device_entry.id in result + assert len(result[device_entry.id]) == 2 + + +async def test_async_get_device_automations_all_devices_condition( + hass, device_reg, entity_reg +): + """Test we get can fetch all the conditions when no device id is passed.""" + await async_setup_component(hass, "device_automation", {}) + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + 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") + assert device_entry.id in result + assert len(result[device_entry.id]) == 2 + + +async def test_async_get_device_automations_all_devices_action( + hass, device_reg, entity_reg +): + """Test we get can fetch all the actions when no device id is passed.""" + await async_setup_component(hass, "device_automation", {}) + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + 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") + assert device_entry.id in result + assert len(result[device_entry.id]) == 3 + + async def test_websocket_get_trigger_capabilities( hass, hass_ws_client, device_reg, entity_reg ): diff --git a/tests/components/remote/test_device_action.py b/tests/components/remote/test_device_action.py index 1193764da3a..48e741a12a4 100644 --- a/tests/components/remote/test_device_action.py +++ b/tests/components/remote/test_device_action.py @@ -2,9 +2,6 @@ import pytest import homeassistant.components.automation as automation -from homeassistant.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) from homeassistant.components.remote import DOMAIN from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry @@ -12,6 +9,7 @@ from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, diff --git a/tests/components/switch/test_device_action.py b/tests/components/switch/test_device_action.py index 9f8d821e74b..2ccfb26d3ef 100644 --- a/tests/components/switch/test_device_action.py +++ b/tests/components/switch/test_device_action.py @@ -2,9 +2,6 @@ import pytest import homeassistant.components.automation as automation -from homeassistant.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) from homeassistant.components.switch import DOMAIN from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry @@ -12,6 +9,7 @@ from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 4a777fcebb6..49fa11de26c 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -8,14 +8,11 @@ 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 ( - _async_get_device_automations as async_get_device_automations, -) from homeassistant.components.zha import DOMAIN from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component -from tests.common import async_mock_service, mock_coro +from tests.common import async_get_device_automations, async_mock_service, mock_coro from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 SHORT_PRESS = "remote_button_short_press" From 4da451fcf7e25257722b3227f1d4d31411a38bed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 10 Aug 2021 17:16:51 -0500 Subject: [PATCH 135/355] Improve HomeKit Color with Color Temp implementation (#54371) --- .../components/homekit/type_lights.py | 189 ++++----- tests/components/homekit/test_type_lights.py | 390 +++++++++++------- 2 files changed, 307 insertions(+), 272 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 169130a194a..aea760534fd 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -6,13 +6,11 @@ from pyhap.const import CATEGORY_LIGHTBULB from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, - ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, ATTR_SUPPORTED_COLOR_MODES, - COLOR_MODE_COLOR_TEMP, DOMAIN, brightness_supported, color_supported, @@ -25,13 +23,17 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import callback +from homeassistant.helpers.event import async_call_later +from homeassistant.util.color import ( + color_temperature_mired_to_kelvin, + color_temperature_to_hs, +) from .accessories import TYPES, HomeAccessory from .const import ( CHAR_BRIGHTNESS, CHAR_COLOR_TEMPERATURE, CHAR_HUE, - CHAR_NAME, CHAR_ON, CHAR_SATURATION, PROP_MAX_VALUE, @@ -43,6 +45,8 @@ _LOGGER = logging.getLogger(__name__) RGB_COLOR = "rgb_color" +CHANGE_COALESCE_TIME_WINDOW = 0.01 + @TYPES.register("Light") class Light(HomeAccessory): @@ -55,102 +59,78 @@ class Light(HomeAccessory): """Initialize a new Light accessory object.""" super().__init__(*args, category=CATEGORY_LIGHTBULB) - self.chars_primary = [] - self.chars_secondary = [] + self.chars = [] + self._event_timer = None + self._pending_events = {} state = self.hass.states.get(self.entity_id) attributes = state.attributes color_modes = attributes.get(ATTR_SUPPORTED_COLOR_MODES) - self.is_color_supported = color_supported(color_modes) - self.is_color_temp_supported = color_temp_supported(color_modes) - self.color_and_temp_supported = ( - self.is_color_supported and self.is_color_temp_supported - ) - self.is_brightness_supported = brightness_supported(color_modes) + self.color_supported = color_supported(color_modes) + self.color_temp_supported = color_temp_supported(color_modes) + self.brightness_supported = brightness_supported(color_modes) - if self.is_brightness_supported: - self.chars_primary.append(CHAR_BRIGHTNESS) + if self.brightness_supported: + self.chars.append(CHAR_BRIGHTNESS) - if self.is_color_supported: - self.chars_primary.append(CHAR_HUE) - self.chars_primary.append(CHAR_SATURATION) + if self.color_supported: + self.chars.extend([CHAR_HUE, CHAR_SATURATION]) - if self.is_color_temp_supported: - if self.color_and_temp_supported: - self.chars_primary.append(CHAR_NAME) - self.chars_secondary.append(CHAR_NAME) - self.chars_secondary.append(CHAR_COLOR_TEMPERATURE) - if self.is_brightness_supported: - self.chars_secondary.append(CHAR_BRIGHTNESS) - else: - self.chars_primary.append(CHAR_COLOR_TEMPERATURE) + if self.color_temp_supported: + self.chars.append(CHAR_COLOR_TEMPERATURE) - serv_light_primary = self.add_preload_service( - SERV_LIGHTBULB, self.chars_primary - ) - serv_light_secondary = None - self.char_on_primary = serv_light_primary.configure_char(CHAR_ON, value=0) + serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars) + self.char_on = serv_light.configure_char(CHAR_ON, value=0) - if self.color_and_temp_supported: - serv_light_secondary = self.add_preload_service( - SERV_LIGHTBULB, self.chars_secondary - ) - serv_light_primary.add_linked_service(serv_light_secondary) - serv_light_primary.configure_char(CHAR_NAME, value="RGB") - self.char_on_secondary = serv_light_secondary.configure_char( - CHAR_ON, value=0 - ) - serv_light_secondary.configure_char(CHAR_NAME, value="Temperature") - - if self.is_brightness_supported: + if self.brightness_supported: # Initial value is set to 100 because 0 is a special value (off). 100 is # an arbitrary non-zero value. It is updated immediately by async_update_state # to set to the correct initial value. - self.char_brightness_primary = serv_light_primary.configure_char( - CHAR_BRIGHTNESS, value=100 - ) - if self.chars_secondary: - self.char_brightness_secondary = serv_light_secondary.configure_char( - CHAR_BRIGHTNESS, value=100 - ) + self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100) - if self.is_color_temp_supported: + if self.color_temp_supported: min_mireds = attributes.get(ATTR_MIN_MIREDS, 153) max_mireds = attributes.get(ATTR_MAX_MIREDS, 500) - serv_light = serv_light_secondary or serv_light_primary - self.char_color_temperature = serv_light.configure_char( + self.char_color_temp = serv_light.configure_char( CHAR_COLOR_TEMPERATURE, value=min_mireds, properties={PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds}, ) - if self.is_color_supported: - self.char_hue = serv_light_primary.configure_char(CHAR_HUE, value=0) - self.char_saturation = serv_light_primary.configure_char( - CHAR_SATURATION, value=75 - ) + if self.color_supported: + self.char_hue = serv_light.configure_char(CHAR_HUE, value=0) + self.char_saturation = serv_light.configure_char(CHAR_SATURATION, value=75) self.async_update_state(state) + serv_light.setter_callback = self._set_chars - if self.color_and_temp_supported: - serv_light_primary.setter_callback = self._set_chars_primary - serv_light_secondary.setter_callback = self._set_chars_secondary - else: - serv_light_primary.setter_callback = self._set_chars + def _set_chars(self, char_values): + _LOGGER.debug("Light _set_chars: %s", char_values) + # Newest change always wins + if CHAR_COLOR_TEMPERATURE in self._pending_events and ( + CHAR_SATURATION in char_values or CHAR_HUE in char_values + ): + del self._pending_events[CHAR_COLOR_TEMPERATURE] + for char in (CHAR_HUE, CHAR_SATURATION): + if char in self._pending_events and CHAR_COLOR_TEMPERATURE in char_values: + del self._pending_events[char] - def _set_chars_primary(self, char_values): - """Primary service is RGB or W if only color or color temp is supported.""" - self._set_chars(char_values, True) + self._pending_events.update(char_values) + if self._event_timer: + self._event_timer() + self._event_timer = async_call_later( + self.hass, CHANGE_COALESCE_TIME_WINDOW, self._send_events + ) - def _set_chars_secondary(self, char_values): - """Secondary service is W if both color or color temp are supported.""" - self._set_chars(char_values, False) - - def _set_chars(self, char_values, is_primary=None): - _LOGGER.debug("Light _set_chars: %s, is_primary: %s", char_values, is_primary) + def _send_events(self, *_): + """Process all changes at once.""" + _LOGGER.debug("Coalesced _set_chars: %s", self._pending_events) + char_values = self._pending_events + self._pending_events = {} events = [] service = SERVICE_TURN_ON params = {ATTR_ENTITY_ID: self.entity_id} + if CHAR_ON in char_values: if not char_values[CHAR_ON]: service = SERVICE_TURN_OFF @@ -170,24 +150,16 @@ class Light(HomeAccessory): ) return - if self.is_color_temp_supported and ( - is_primary is False or CHAR_COLOR_TEMPERATURE in char_values - ): - params[ATTR_COLOR_TEMP] = char_values.get( - CHAR_COLOR_TEMPERATURE, self.char_color_temperature.value - ) + if CHAR_COLOR_TEMPERATURE in char_values: + params[ATTR_COLOR_TEMP] = char_values[CHAR_COLOR_TEMPERATURE] events.append(f"color temperature at {params[ATTR_COLOR_TEMP]}") - if self.is_color_supported and ( - is_primary is True - or (CHAR_HUE in char_values and CHAR_SATURATION in char_values) - ): - color = ( + elif CHAR_HUE in char_values or CHAR_SATURATION in char_values: + color = params[ATTR_HS_COLOR] = ( char_values.get(CHAR_HUE, self.char_hue.value), char_values.get(CHAR_SATURATION, self.char_saturation.value), ) _LOGGER.debug("%s: Set hs_color to %s", self.entity_id, color) - params[ATTR_HS_COLOR] = color events.append(f"set color at {color}") self.async_call_service(DOMAIN, service, params, ", ".join(events)) @@ -198,20 +170,10 @@ class Light(HomeAccessory): # Handle State state = new_state.state attributes = new_state.attributes - char_on_value = int(state == STATE_ON) - - if self.color_and_temp_supported: - color_mode = attributes.get(ATTR_COLOR_MODE) - color_temp_mode = color_mode == COLOR_MODE_COLOR_TEMP - primary_on_value = char_on_value if not color_temp_mode else 0 - secondary_on_value = char_on_value if color_temp_mode else 0 - self.char_on_primary.set_value(primary_on_value) - self.char_on_secondary.set_value(secondary_on_value) - else: - self.char_on_primary.set_value(char_on_value) + self.char_on.set_value(int(state == STATE_ON)) # Handle Brightness - if self.is_brightness_supported: + if self.brightness_supported: brightness = attributes.get(ATTR_BRIGHTNESS) if isinstance(brightness, (int, float)): brightness = round(brightness / 255 * 100, 0) @@ -227,22 +189,25 @@ class Light(HomeAccessory): # order to avoid this incorrect behavior. if brightness == 0 and state == STATE_ON: brightness = 1 - self.char_brightness_primary.set_value(brightness) - if self.color_and_temp_supported: - self.char_brightness_secondary.set_value(brightness) + self.char_brightness.set_value(brightness) + + # Handle Color - color must always be set before color temperature + # or the iOS UI will not display it correctly. + if self.color_supported: + if ATTR_COLOR_TEMP in attributes: + hue, saturation = color_temperature_to_hs( + color_temperature_mired_to_kelvin( + new_state.attributes[ATTR_COLOR_TEMP] + ) + ) + else: + hue, saturation = attributes.get(ATTR_HS_COLOR, (None, None)) + if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)): + self.char_hue.set_value(round(hue, 0)) + self.char_saturation.set_value(round(saturation, 0)) # Handle color temperature - if self.is_color_temp_supported: - color_temperature = attributes.get(ATTR_COLOR_TEMP) - if isinstance(color_temperature, (int, float)): - color_temperature = round(color_temperature, 0) - self.char_color_temperature.set_value(color_temperature) - - # Handle Color - if self.is_color_supported: - hue, saturation = attributes.get(ATTR_HS_COLOR, (None, None)) - if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)): - hue = round(hue, 0) - saturation = round(saturation, 0) - self.char_hue.set_value(hue) - self.char_saturation.set_value(saturation) + if self.color_temp_supported: + color_temp = attributes.get(ATTR_COLOR_TEMP) + if isinstance(color_temp, (int, float)): + self.char_color_temp.set_value(round(color_temp, 0)) diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index f75e6bf19ac..90e3aa0cabe 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -1,21 +1,21 @@ """Test different accessory types: Lights.""" +from datetime import timedelta + from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE import pytest from homeassistant.components.homekit.const import ATTR_VALUE -from homeassistant.components.homekit.type_lights import Light +from homeassistant.components.homekit.type_lights import ( + CHANGE_COALESCE_TIME_WINDOW, + Light, +) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, - ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_SUPPORTED_COLOR_MODES, - COLOR_MODE_COLOR_TEMP, - COLOR_MODE_HS, - COLOR_MODE_RGB, - COLOR_MODE_XY, DOMAIN, ) from homeassistant.const import ( @@ -29,8 +29,16 @@ from homeassistant.const import ( ) from homeassistant.core import CoreState from homeassistant.helpers import entity_registry as er +import homeassistant.util.dt as dt_util -from tests.common import async_mock_service +from tests.common import async_fire_time_changed, async_mock_service + + +async def _wait_for_light_coalesce(hass): + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=CHANGE_COALESCE_TIME_WINDOW) + ) + await hass.async_block_till_done() async def test_light_basic(hass, hk_driver, events): @@ -44,45 +52,41 @@ async def test_light_basic(hass, hk_driver, events): assert acc.aid == 1 assert acc.category == 5 # Lightbulb - assert acc.char_on_primary.value + assert acc.char_on.value await acc.run() await hass.async_block_till_done() - assert acc.char_on_primary.value == 1 + assert acc.char_on.value == 1 hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() - assert acc.char_on_primary.value == 0 + assert acc.char_on.value == 0 hass.states.async_set(entity_id, STATE_UNKNOWN) await hass.async_block_till_done() - assert acc.char_on_primary.value == 0 + assert acc.char_on.value == 0 hass.states.async_remove(entity_id) await hass.async_block_till_done() - assert acc.char_on_primary.value == 0 + assert acc.char_on.value == 0 # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") call_turn_off = async_mock_service(hass, DOMAIN, "turn_off") - char_on_primary_iid = acc.char_on_primary.to_HAP()[HAP_REPR_IID] + char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] hk_driver.set_characteristics( { HAP_REPR_CHARS: [ - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_on_primary_iid, - HAP_REPR_VALUE: 1, - } + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1} ] }, "mock_addr", ) - await hass.async_add_executor_job(acc.char_on_primary.client_update_value, 1) - await hass.async_block_till_done() + acc.char_on.client_update_value(1) + await _wait_for_light_coalesce(hass) assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 1 @@ -94,16 +98,12 @@ async def test_light_basic(hass, hk_driver, events): hk_driver.set_characteristics( { HAP_REPR_CHARS: [ - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_on_primary_iid, - HAP_REPR_VALUE: 0, - } + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 0} ] }, "mock_addr", ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 2 @@ -128,17 +128,17 @@ async def test_light_brightness(hass, hk_driver, events, supported_color_modes): # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the # brightness to 100 when turning on a light on a freshly booted up server. - assert acc.char_brightness_primary.value != 0 - char_on_primary_iid = acc.char_on_primary.to_HAP()[HAP_REPR_IID] - char_brightness_primary_iid = acc.char_brightness_primary.to_HAP()[HAP_REPR_IID] + assert acc.char_brightness.value != 0 + char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] + char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] await acc.run() await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 100 + assert acc.char_brightness.value == 100 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 40 + assert acc.char_brightness.value == 40 # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") @@ -147,21 +147,17 @@ async def test_light_brightness(hass, hk_driver, events, supported_color_modes): hk_driver.set_characteristics( { HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_on_primary_iid, - HAP_REPR_VALUE: 1, - }, - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_brightness_primary_iid, + HAP_REPR_IID: char_brightness_iid, HAP_REPR_VALUE: 20, }, ] }, "mock_addr", ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) assert call_turn_on[0] assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20 @@ -173,21 +169,17 @@ async def test_light_brightness(hass, hk_driver, events, supported_color_modes): hk_driver.set_characteristics( { HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_on_primary_iid, - HAP_REPR_VALUE: 1, - }, - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_brightness_primary_iid, + HAP_REPR_IID: char_brightness_iid, HAP_REPR_VALUE: 40, }, ] }, "mock_addr", ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) assert call_turn_on[1] assert call_turn_on[1].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[1].data[ATTR_BRIGHTNESS_PCT] == 40 @@ -199,21 +191,17 @@ async def test_light_brightness(hass, hk_driver, events, supported_color_modes): hk_driver.set_characteristics( { HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_on_primary_iid, - HAP_REPR_VALUE: 1, - }, - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_brightness_primary_iid, + HAP_REPR_IID: char_brightness_iid, HAP_REPR_VALUE: 0, }, ] }, "mock_addr", ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 3 @@ -223,24 +211,24 @@ async def test_light_brightness(hass, hk_driver, events, supported_color_modes): # in update_state hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0}) await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 1 + assert acc.char_brightness.value == 1 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 255}) await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 100 + assert acc.char_brightness.value == 100 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0}) await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 1 + assert acc.char_brightness.value == 1 # Ensure floats are handled hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 55.66}) await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 22 + assert acc.char_brightness.value == 22 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 108.4}) await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 43 + assert acc.char_brightness.value == 43 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0.0}) await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 1 + assert acc.char_brightness.value == 1 async def test_light_color_temperature(hass, hk_driver, events): @@ -256,33 +244,30 @@ async def test_light_color_temperature(hass, hk_driver, events): acc = Light(hass, hk_driver, "Light", entity_id, 1, None) hk_driver.add_accessory(acc) - assert acc.char_color_temperature.value == 190 + assert acc.char_color_temp.value == 190 await acc.run() await hass.async_block_till_done() - assert acc.char_color_temperature.value == 190 + assert acc.char_color_temp.value == 190 # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") - char_color_temperature_iid = acc.char_color_temperature.to_HAP()[HAP_REPR_IID] + char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID] hk_driver.set_characteristics( { HAP_REPR_CHARS: [ { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_color_temperature_iid, + HAP_REPR_IID: char_color_temp_iid, HAP_REPR_VALUE: 250, } ] }, "mock_addr", ) - await hass.async_add_executor_job( - acc.char_color_temperature.client_update_value, 250 - ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_COLOR_TEMP] == 250 @@ -292,11 +277,7 @@ async def test_light_color_temperature(hass, hk_driver, events): @pytest.mark.parametrize( "supported_color_modes", - [ - [COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS], - [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGB], - [COLOR_MODE_COLOR_TEMP, COLOR_MODE_XY], - ], + [["color_temp", "hs"], ["color_temp", "rgb"], ["color_temp", "xy"]], ) async def test_light_color_temperature_and_rgb_color( hass, hk_driver, events, supported_color_modes @@ -310,93 +291,190 @@ async def test_light_color_temperature_and_rgb_color( { ATTR_SUPPORTED_COLOR_MODES: supported_color_modes, ATTR_COLOR_TEMP: 190, - ATTR_BRIGHTNESS: 255, - ATTR_COLOR_MODE: COLOR_MODE_RGB, ATTR_HS_COLOR: (260, 90), }, ) await hass.async_block_till_done() acc = Light(hass, hk_driver, "Light", entity_id, 1, None) - assert acc.char_hue.value == 260 - assert acc.char_saturation.value == 90 - assert acc.char_on_primary.value == 1 - assert acc.char_on_secondary.value == 0 - assert acc.char_brightness_primary.value == 100 - assert acc.char_brightness_secondary.value == 100 - - assert hasattr(acc, "char_color_temperature") - - hass.states.async_set( - entity_id, - STATE_ON, - { - ATTR_COLOR_TEMP: 224, - ATTR_COLOR_MODE: COLOR_MODE_COLOR_TEMP, - ATTR_BRIGHTNESS: 127, - }, - ) - await hass.async_block_till_done() - await acc.run() - await hass.async_block_till_done() - assert acc.char_color_temperature.value == 224 - assert acc.char_on_primary.value == 0 - assert acc.char_on_secondary.value == 1 - assert acc.char_brightness_primary.value == 50 - assert acc.char_brightness_secondary.value == 50 - - hass.states.async_set( - entity_id, - STATE_ON, - { - ATTR_COLOR_TEMP: 352, - ATTR_COLOR_MODE: COLOR_MODE_COLOR_TEMP, - }, - ) - await hass.async_block_till_done() - await acc.run() - await hass.async_block_till_done() - assert acc.char_color_temperature.value == 352 - assert acc.char_on_primary.value == 0 - assert acc.char_on_secondary.value == 1 hk_driver.add_accessory(acc) + assert acc.char_color_temp.value == 190 + assert acc.char_hue.value == 27 + assert acc.char_saturation.value == 16 + + assert hasattr(acc, "char_color_temp") + + hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: 224}) + await hass.async_block_till_done() + await acc.run() + await hass.async_block_till_done() + assert acc.char_color_temp.value == 224 + assert acc.char_hue.value == 27 + assert acc.char_saturation.value == 27 + + hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: 352}) + await hass.async_block_till_done() + await acc.run() + await hass.async_block_till_done() + assert acc.char_color_temp.value == 352 + assert acc.char_hue.value == 28 + assert acc.char_saturation.value == 61 + + char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] + char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID] char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID] - char_color_temperature_iid = acc.char_color_temperature.to_HAP()[HAP_REPR_IID] + char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID] + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_iid, + HAP_REPR_VALUE: 20, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_color_temp_iid, + HAP_REPR_VALUE: 250, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_hue_iid, + HAP_REPR_VALUE: 50, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_saturation_iid, + HAP_REPR_VALUE: 50, + }, + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on[0] + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20 + assert call_turn_on[0].data[ATTR_COLOR_TEMP] == 250 + + assert len(events) == 1 + assert ( + events[-1].data[ATTR_VALUE] + == f"Set state to 1, brightness at 20{PERCENTAGE}, color temperature at 250" + ) + + # Only set Hue hk_driver.set_characteristics( { HAP_REPR_CHARS: [ { HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_hue_iid, - HAP_REPR_VALUE: 145, - }, - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_saturation_iid, - HAP_REPR_VALUE: 75, - }, + HAP_REPR_VALUE: 30, + } ] }, "mock_addr", ) - assert acc.char_hue.value == 145 - assert acc.char_saturation.value == 75 + await _wait_for_light_coalesce(hass) + assert call_turn_on[1] + assert call_turn_on[1].data[ATTR_HS_COLOR] == (30, 50) + assert events[-1].data[ATTR_VALUE] == "set color at (30, 50)" + + # Only set Saturation hk_driver.set_characteristics( { HAP_REPR_CHARS: [ { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_color_temperature_iid, - HAP_REPR_VALUE: 200, - }, + HAP_REPR_IID: char_saturation_iid, + HAP_REPR_VALUE: 20, + } ] }, "mock_addr", ) - assert acc.char_color_temperature.value == 200 + await _wait_for_light_coalesce(hass) + assert call_turn_on[2] + assert call_turn_on[2].data[ATTR_HS_COLOR] == (30, 20) + + assert events[-1].data[ATTR_VALUE] == "set color at (30, 20)" + + # Generate a conflict by setting hue and then color temp + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_hue_iid, + HAP_REPR_VALUE: 80, + } + ] + }, + "mock_addr", + ) + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_color_temp_iid, + HAP_REPR_VALUE: 320, + } + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on[3] + assert call_turn_on[3].data[ATTR_COLOR_TEMP] == 320 + assert events[-1].data[ATTR_VALUE] == "color temperature at 320" + + # Generate a conflict by setting color temp then saturation + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_color_temp_iid, + HAP_REPR_VALUE: 404, + } + ] + }, + "mock_addr", + ) + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_saturation_iid, + HAP_REPR_VALUE: 35, + } + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on[4] + assert call_turn_on[4].data[ATTR_HS_COLOR] == (80, 35) + assert events[-1].data[ATTR_VALUE] == "set color at (80, 35)" + + # Set from HASS + hass.states.async_set(entity_id, STATE_ON, {ATTR_HS_COLOR: (100, 100)}) + await hass.async_block_till_done() + await acc.run() + await hass.async_block_till_done() + assert acc.char_color_temp.value == 404 + assert acc.char_hue.value == 100 + assert acc.char_saturation.value == 100 @pytest.mark.parametrize("supported_color_modes", [["hs"], ["rgb"], ["xy"]]) @@ -444,7 +522,7 @@ async def test_light_rgb_color(hass, hk_driver, events, supported_color_modes): }, "mock_addr", ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_HS_COLOR] == (145, 75) @@ -476,13 +554,13 @@ async def test_light_restore(hass, hk_driver, events): hk_driver.add_accessory(acc) assert acc.category == 5 # Lightbulb - assert acc.chars_primary == [] - assert acc.char_on_primary.value == 0 + assert acc.chars == [] + assert acc.char_on.value == 0 acc = Light(hass, hk_driver, "Light", "light.all_info_set", 2, None) assert acc.category == 5 # Lightbulb - assert acc.chars_primary == ["Brightness"] - assert acc.char_on_primary.value == 0 + assert acc.chars == ["Brightness"] + assert acc.char_on.value == 0 async def test_light_set_brightness_and_color(hass, hk_driver, events): @@ -503,19 +581,19 @@ async def test_light_set_brightness_and_color(hass, hk_driver, events): # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the # brightness to 100 when turning on a light on a freshly booted up server. - assert acc.char_brightness_primary.value != 0 - char_on_primary_iid = acc.char_on_primary.to_HAP()[HAP_REPR_IID] - char_brightness_primary_iid = acc.char_brightness_primary.to_HAP()[HAP_REPR_IID] + assert acc.char_brightness.value != 0 + char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] + char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID] char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID] await acc.run() await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 100 + assert acc.char_brightness.value == 100 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 40 + assert acc.char_brightness.value == 40 hass.states.async_set(entity_id, STATE_ON, {ATTR_HS_COLOR: (4.5, 9.2)}) await hass.async_block_till_done() @@ -528,14 +606,10 @@ async def test_light_set_brightness_and_color(hass, hk_driver, events): hk_driver.set_characteristics( { HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_on_primary_iid, - HAP_REPR_VALUE: 1, - }, - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_brightness_primary_iid, + HAP_REPR_IID: char_brightness_iid, HAP_REPR_VALUE: 20, }, { @@ -552,7 +626,7 @@ async def test_light_set_brightness_and_color(hass, hk_driver, events): }, "mock_addr", ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) assert call_turn_on[0] assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20 @@ -583,22 +657,22 @@ async def test_light_set_brightness_and_color_temp(hass, hk_driver, events): # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the # brightness to 100 when turning on a light on a freshly booted up server. - assert acc.char_brightness_primary.value != 0 - char_on_primary_iid = acc.char_on_primary.to_HAP()[HAP_REPR_IID] - char_brightness_primary_iid = acc.char_brightness_primary.to_HAP()[HAP_REPR_IID] - char_color_temperature_iid = acc.char_color_temperature.to_HAP()[HAP_REPR_IID] + assert acc.char_brightness.value != 0 + char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] + char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] + char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID] await acc.run() await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 100 + assert acc.char_brightness.value == 100 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 40 + assert acc.char_brightness.value == 40 hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: (224.14)}) await hass.async_block_till_done() - assert acc.char_color_temperature.value == 224 + assert acc.char_color_temp.value == 224 # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") @@ -606,26 +680,22 @@ async def test_light_set_brightness_and_color_temp(hass, hk_driver, events): hk_driver.set_characteristics( { HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_on_primary_iid, - HAP_REPR_VALUE: 1, - }, - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_brightness_primary_iid, + HAP_REPR_IID: char_brightness_iid, HAP_REPR_VALUE: 20, }, { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_color_temperature_iid, + HAP_REPR_IID: char_color_temp_iid, HAP_REPR_VALUE: 250, }, ] }, "mock_addr", ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) assert call_turn_on[0] assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20 From 4ae6435a6413932e67ad4492aa8dc3c00dbd0a1a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 10 Aug 2021 17:17:49 -0500 Subject: [PATCH 136/355] Avoid increasing yeelight rate limit when the state is already set (#54410) --- homeassistant/components/yeelight/light.py | 35 ++++++- tests/components/yeelight/test_light.py | 112 ++++++++++++++++++++- 2 files changed, 144 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index d2ddc92bb8d..b714ddfaba8 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +import math import voluptuous as vol import yeelight @@ -576,6 +577,13 @@ class YeelightGenericLight(YeelightEntity, LightEntity): async def async_set_brightness(self, brightness, duration) -> None: """Set bulb brightness.""" if brightness: + if math.floor(self.brightness) == math.floor(brightness): + _LOGGER.debug("brightness already set to: %s", brightness) + # Already set, and since we get pushed updates + # we avoid setting it again to ensure we do not + # hit the rate limit + return + _LOGGER.debug("Setting brightness: %s", brightness) await self._bulb.async_set_brightness( brightness / 255 * 100, duration=duration, light_type=self.light_type @@ -585,6 +593,13 @@ class YeelightGenericLight(YeelightEntity, LightEntity): async def async_set_hs(self, hs_color, duration) -> None: """Set bulb's color.""" if hs_color and COLOR_MODE_HS in self.supported_color_modes: + if self.color_mode == COLOR_MODE_HS and self.hs_color == hs_color: + _LOGGER.debug("HS already set to: %s", hs_color) + # Already set, and since we get pushed updates + # we avoid setting it again to ensure we do not + # hit the rate limit + return + _LOGGER.debug("Setting HS: %s", hs_color) await self._bulb.async_set_hsv( hs_color[0], hs_color[1], duration=duration, light_type=self.light_type @@ -594,9 +609,16 @@ class YeelightGenericLight(YeelightEntity, LightEntity): async def async_set_rgb(self, rgb, duration) -> None: """Set bulb's color.""" if rgb and COLOR_MODE_RGB in self.supported_color_modes: + if self.color_mode == COLOR_MODE_RGB and self.rgb_color == rgb: + _LOGGER.debug("RGB already set to: %s", rgb) + # Already set, and since we get pushed updates + # we avoid setting it again to ensure we do not + # hit the rate limit + return + _LOGGER.debug("Setting RGB: %s", rgb) await self._bulb.async_set_rgb( - rgb[0], rgb[1], rgb[2], duration=duration, light_type=self.light_type + *rgb, duration=duration, light_type=self.light_type ) @_async_cmd @@ -604,7 +626,16 @@ class YeelightGenericLight(YeelightEntity, LightEntity): """Set bulb's color temperature.""" if colortemp and COLOR_MODE_COLOR_TEMP in self.supported_color_modes: temp_in_k = mired_to_kelvin(colortemp) - _LOGGER.debug("Setting color temp: %s K", temp_in_k) + + if ( + self.color_mode == COLOR_MODE_COLOR_TEMP + and self.color_temp == colortemp + ): + _LOGGER.debug("Color temp already set to: %s", temp_in_k) + # Already set, and since we get pushed updates + # we avoid setting it again to ensure we do not + # hit the rate limit + return await self._bulb.async_set_color_temp( temp_in_k, duration=duration, light_type=self.light_type diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index 9a1f632242b..8b7ec154b83 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -1,6 +1,6 @@ """Test the Yeelight light.""" import logging -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import ANY, AsyncMock, MagicMock, call, patch from yeelight import ( BulbException, @@ -19,6 +19,7 @@ from yeelight.main import _MODEL_SPECS from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, @@ -428,6 +429,115 @@ async def test_services(hass: HomeAssistant, caplog): ) +async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant): + """Ensure we suppress state changes that will increase the rate limit when there is no change.""" + mocked_bulb = _mocked_bulb() + properties = {**PROPERTIES} + properties.pop("active_mode") + properties["color_mode"] = "3" # HSV + mocked_bulb.last_properties = properties + mocked_bulb.bulb_type = BulbType.Color + config_entry = MockConfigEntry( + domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False} + ) + config_entry.add_to_hass(hass) + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + # We use asyncio.create_task now to avoid + # blocking starting so we need to block again + await hass.async_block_till_done() + + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ENTITY_LIGHT, + ATTR_HS_COLOR: (PROPERTIES["hue"], PROPERTIES["sat"]), + }, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [] + assert mocked_bulb.async_set_rgb.mock_calls == [] + assert mocked_bulb.async_set_color_temp.mock_calls == [] + assert mocked_bulb.async_set_brightness.mock_calls == [] + + mocked_bulb.last_properties["color_mode"] = 1 + rgb = int(PROPERTIES["rgb"]) + blue = rgb & 0xFF + green = (rgb >> 8) & 0xFF + red = (rgb >> 16) & 0xFF + + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_RGB_COLOR: (red, green, blue)}, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [] + assert mocked_bulb.async_set_rgb.mock_calls == [] + assert mocked_bulb.async_set_color_temp.mock_calls == [] + assert mocked_bulb.async_set_brightness.mock_calls == [] + mocked_bulb.async_set_rgb.reset_mock() + + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ENTITY_LIGHT, + ATTR_BRIGHTNESS_PCT: PROPERTIES["current_brightness"], + }, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [] + assert mocked_bulb.async_set_rgb.mock_calls == [] + assert mocked_bulb.async_set_color_temp.mock_calls == [] + assert mocked_bulb.async_set_brightness.mock_calls == [] + + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_COLOR_TEMP: 250}, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [] + assert mocked_bulb.async_set_rgb.mock_calls == [] + # Should call for the color mode change + assert mocked_bulb.async_set_color_temp.mock_calls == [ + call(4000, duration=350, light_type=ANY) + ] + assert mocked_bulb.async_set_brightness.mock_calls == [] + mocked_bulb.async_set_color_temp.reset_mock() + + mocked_bulb.last_properties["color_mode"] = 2 + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_COLOR_TEMP: 250}, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [] + assert mocked_bulb.async_set_rgb.mock_calls == [] + assert mocked_bulb.async_set_color_temp.mock_calls == [] + assert mocked_bulb.async_set_brightness.mock_calls == [] + + mocked_bulb.last_properties["color_mode"] = 3 + # This last change should generate a call even though + # the color mode is the same since the HSV has changed + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_HS_COLOR: (5, 5)}, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [ + call(5.0, 5.0, duration=350, light_type=ANY) + ] + assert mocked_bulb.async_set_rgb.mock_calls == [] + assert mocked_bulb.async_set_color_temp.mock_calls == [] + assert mocked_bulb.async_set_brightness.mock_calls == [] + + async def test_device_types(hass: HomeAssistant, caplog): """Test different device types.""" mocked_bulb = _mocked_bulb() From 390023a576d81ef67f1c52524b4d1df9f25884a9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 11 Aug 2021 00:18:57 +0000 Subject: [PATCH 137/355] [ci skip] Translation update --- .../components/deconz/translations/da.json | 2 +- .../emulated_roku/translations/lt.json | 11 +++++++ .../components/hangouts/translations/lt.json | 12 ++++++++ .../homekit_controller/translations/lt.json | 9 ++++++ .../components/hue/translations/da.json | 10 +++---- .../components/hue/translations/lt.json | 11 +++++++ .../components/mqtt/translations/lt.json | 16 ++++++++++ .../components/nest/translations/da.json | 2 +- .../components/nest/translations/lt.json | 3 ++ .../components/openuv/translations/lt.json | 14 +++++++++ .../components/tractive/translations/cs.json | 19 ++++++++++++ .../components/tractive/translations/fr.json | 19 ++++++++++++ .../uptimerobot/translations/cs.json | 11 ++++++- .../uptimerobot/translations/de.json | 13 ++++++++- .../uptimerobot/translations/fr.json | 29 +++++++++++++++++++ .../uptimerobot/translations/he.json | 7 +++++ .../uptimerobot/translations/hu.json | 13 ++++++++- 17 files changed, 191 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/emulated_roku/translations/lt.json create mode 100644 homeassistant/components/hangouts/translations/lt.json create mode 100644 homeassistant/components/homekit_controller/translations/lt.json create mode 100644 homeassistant/components/hue/translations/lt.json create mode 100644 homeassistant/components/mqtt/translations/lt.json create mode 100644 homeassistant/components/openuv/translations/lt.json create mode 100644 homeassistant/components/tractive/translations/cs.json create mode 100644 homeassistant/components/tractive/translations/fr.json create mode 100644 homeassistant/components/uptimerobot/translations/fr.json diff --git a/homeassistant/components/deconz/translations/da.json b/homeassistant/components/deconz/translations/da.json index be165a206bf..00e054aecc9 100644 --- a/homeassistant/components/deconz/translations/da.json +++ b/homeassistant/components/deconz/translations/da.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Bridge er allerede konfigureret", "already_in_progress": "Konfigurationsflow for bro er allerede i gang.", - "no_bridges": "Ingen deConz-bridge fundet", + "no_bridges": "Ingen deConz-bro fundet", "not_deconz_bridge": "Ikke en deCONZ-bro", "updated_instance": "Opdaterede deCONZ-instans med ny v\u00e6rtadresse" }, diff --git a/homeassistant/components/emulated_roku/translations/lt.json b/homeassistant/components/emulated_roku/translations/lt.json new file mode 100644 index 00000000000..8ae517ecfbe --- /dev/null +++ b/homeassistant/components/emulated_roku/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host_ip": "Hosto IP adresas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/lt.json b/homeassistant/components/hangouts/translations/lt.json new file mode 100644 index 00000000000..13dbbf8bdbc --- /dev/null +++ b/homeassistant/components/hangouts/translations/lt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "2fa": { + "data": { + "2fa": "2FA PIN" + }, + "title": "2 veiksni\u0173 autentifikavimas" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/lt.json b/homeassistant/components/homekit_controller/translations/lt.json new file mode 100644 index 00000000000..965b32b366d --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/lt.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "\u012erenginio pasirinkimas" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/da.json b/homeassistant/components/hue/translations/da.json index 031076172ac..f081a912dd7 100644 --- a/homeassistant/components/hue/translations/da.json +++ b/homeassistant/components/hue/translations/da.json @@ -2,22 +2,22 @@ "config": { "abort": { "all_configured": "Alle Philips Hue-broer er allerede konfigureret", - "already_configured": "Bridgen er allerede konfigureret", + "already_configured": "Enhed er allerede konfigureret", "already_in_progress": "Bro-konfiguration er allerede i gang.", - "cannot_connect": "Kunne ikke oprette forbindelse til bridgen", + "cannot_connect": "Kunne ikke oprette forbindelse", "discover_timeout": "Ingen Philips Hue-bro fundet", "no_bridges": "Ingen Philips Hue-broer fundet", "not_hue_bridge": "Ikke en Hue-bro", - "unknown": "Ukendt fejl opstod" + "unknown": "Uventet fejl" }, "error": { - "linking": "Der opstod en ukendt linkfejl.", + "linking": "Der opstod en uventet fejl", "register_failed": "Det lykkedes ikke at registrere, pr\u00f8v igen" }, "step": { "init": { "data": { - "host": "V\u00e6rt" + "host": "Server" }, "title": "V\u00e6lg Hue bridge" }, diff --git a/homeassistant/components/hue/translations/lt.json b/homeassistant/components/hue/translations/lt.json new file mode 100644 index 00000000000..1e12894085b --- /dev/null +++ b/homeassistant/components/hue/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "init": { + "data": { + "host": "Hostas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/lt.json b/homeassistant/components/mqtt/translations/lt.json new file mode 100644 index 00000000000..35257770c75 --- /dev/null +++ b/homeassistant/components/mqtt/translations/lt.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepavyko prisijungti" + }, + "step": { + "broker": { + "data": { + "password": "Slapta\u017eodis", + "port": "Portas", + "username": "Prisijungimo vardas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/da.json b/homeassistant/components/nest/translations/da.json index 054b4442506..5224e7a660d 100644 --- a/homeassistant/components/nest/translations/da.json +++ b/homeassistant/components/nest/translations/da.json @@ -14,7 +14,7 @@ "flow_impl": "Udbyder" }, "description": "V\u00e6lg hvilken godkendelsesudbyder du vil godkende med Nest.", - "title": "Godkendelsesudbyder" + "title": "Identitetsudbyder" }, "link": { "data": { diff --git a/homeassistant/components/nest/translations/lt.json b/homeassistant/components/nest/translations/lt.json index 3cac49e3871..629b65d347d 100644 --- a/homeassistant/components/nest/translations/lt.json +++ b/homeassistant/components/nest/translations/lt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Nenumatyta klaida" + }, "step": { "link": { "data": { diff --git a/homeassistant/components/openuv/translations/lt.json b/homeassistant/components/openuv/translations/lt.json new file mode 100644 index 00000000000..1546651d54a --- /dev/null +++ b/homeassistant/components/openuv/translations/lt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_api_key": "Neteisingas API raktas" + }, + "step": { + "user": { + "data": { + "api_key": "API raktas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/cs.json b/homeassistant/components/tractive/translations/cs.json new file mode 100644 index 00000000000..de52bfbd7a8 --- /dev/null +++ b/homeassistant/components/tractive/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "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/tractive/translations/fr.json b/homeassistant/components/tractive/translations/fr.json new file mode 100644 index 00000000000..1d3c15c13d5 --- /dev/null +++ b/homeassistant/components/tractive/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositif d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "email": "Adresse mail", + "password": "Mot de passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/cs.json b/homeassistant/components/uptimerobot/translations/cs.json index 7261d6146fb..dc5ccb72741 100644 --- a/homeassistant/components/uptimerobot/translations/cs.json +++ b/homeassistant/components/uptimerobot/translations/cs.json @@ -1,13 +1,22 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Kl\u00ed\u010d API" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, "user": { "data": { "api_key": "Kl\u00ed\u010d API" diff --git a/homeassistant/components/uptimerobot/translations/de.json b/homeassistant/components/uptimerobot/translations/de.json index 7a50a5ba28e..a25f58dfe0c 100644 --- a/homeassistant/components/uptimerobot/translations/de.json +++ b/homeassistant/components/uptimerobot/translations/de.json @@ -2,18 +2,29 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_failed_existing": "Der Konfigurationseintrag konnte nicht aktualisiert werden. Bitte entferne die Integration und richte sie erneut ein.", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "unknown": "Unerwarteter Fehler" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", + "reauth_failed_matching_account": "Der von dir angegebene API-Schl\u00fcssel stimmt nicht mit der Konto-ID f\u00fcr die vorhandene Konfiguration \u00fcberein.", "unknown": "Unerwarteter Fehler" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API-Schl\u00fcssel" + }, + "description": "Du musst einen neuen schreibgesch\u00fctzten API-Schl\u00fcssel von Uptime Robot bereitstellen.", + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "api_key": "API-Schl\u00fcssel" - } + }, + "description": "Du musst einen schreibgesch\u00fctzten API-Schl\u00fcssel von Uptime Robot bereitstellen." } } } diff --git a/homeassistant/components/uptimerobot/translations/fr.json b/homeassistant/components/uptimerobot/translations/fr.json new file mode 100644 index 00000000000..2b4322bb410 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/fr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Compte d\u00e9j\u00e0 configur\u00e9", + "reauth_failed_existing": "Impossible de mettre \u00e0 jour l'entr\u00e9e de configuration, veuillez supprimer l'int\u00e9gration et la configurer \u00e0 nouveau.", + "unknown": "Erreur inattendue" + }, + "error": { + "cannot_connect": "Echec de la connexion", + "invalid_api_key": "Cl\u00e9 API non valide", + "reauth_failed_matching_account": "La cl\u00e9 API que vous avez fournie ne correspond pas \u00e0 l\u2019ID de compte pour la configuration existante.", + "unknown": "Erreur inattendue" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Cl\u00e9 API" + }, + "description": "Vous devez fournir une nouvelle cl\u00e9 API en lecture seule \u00e0 partir d'Uptime Robot" + }, + "user": { + "data": { + "api_key": "Cl\u00e9 API" + }, + "description": "Vous devez fournir une cl\u00e9 API en lecture seule \u00e0 partir d'Uptime Robot" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/he.json b/homeassistant/components/uptimerobot/translations/he.json index 1a45e5c78cd..07de294aea4 100644 --- a/homeassistant/components/uptimerobot/translations/he.json +++ b/homeassistant/components/uptimerobot/translations/he.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\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", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { @@ -10,6 +11,12 @@ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, "user": { "data": { "api_key": "\u05de\u05e4\u05ea\u05d7 API" diff --git a/homeassistant/components/uptimerobot/translations/hu.json b/homeassistant/components/uptimerobot/translations/hu.json index b9e14001679..000851093a5 100644 --- a/homeassistant/components/uptimerobot/translations/hu.json +++ b/homeassistant/components/uptimerobot/translations/hu.json @@ -2,18 +2,29 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_failed_existing": "Nem siker\u00fclt friss\u00edteni a konfigur\u00e1ci\u00f3s bejegyz\u00e9st. K\u00e9rj\u00fck, t\u00e1vol\u00edtsa el az integr\u00e1ci\u00f3t, \u00e9s \u00e1ll\u00edtsa be \u00fajra.", + "reauth_successful": "Az ism\u00e9telt hiteles\u00edt\u00e9s sikeres volt", "unknown": "V\u00e1ratlan hiba" }, "error": { "cannot_connect": "Nem siker\u00fclt csatlakozni", "invalid_api_key": "\u00c9rv\u00e9nytelen API-kulcs", + "reauth_failed_matching_account": "A megadott API -kulcs nem egyezik a megl\u00e9v\u0151 konfigur\u00e1ci\u00f3 fi\u00f3kazonos\u00edt\u00f3j\u00e1val.", "unknown": "V\u00e1ratlan hiba" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API kulcs" + }, + "description": "Meg kell adnia egy \u00faj, csak olvashat\u00f3 API-kulcsot az Uptime Robot-t\u00f3l", + "title": "Integr\u00e1ci\u00f3 \u00fajb\u00f3li hiteles\u00edt\u00e9se" + }, "user": { "data": { "api_key": "API kulcs" - } + }, + "description": "Meg kell adnia egy csak olvashat\u00f3 API-kulcsot az Uptime Robot-t\u00f3l" } } } From e99576c0947e710fa40967458c3c01ed1ba20872 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 10 Aug 2021 19:33:06 -0500 Subject: [PATCH 138/355] Pass width and height when requesting camera snapshot (#53835) --- homeassistant/components/abode/camera.py | 6 +- homeassistant/components/amcrest/camera.py | 6 +- homeassistant/components/arlo/camera.py | 6 +- homeassistant/components/august/camera.py | 5 +- homeassistant/components/blink/camera.py | 6 +- homeassistant/components/bloomsky/camera.py | 6 +- homeassistant/components/buienradar/camera.py | 4 +- homeassistant/components/camera/__init__.py | 108 +++++++++++++++--- .../{homekit => camera}/img_util.py | 34 ++++-- homeassistant/components/camera/manifest.json | 1 + homeassistant/components/canary/camera.py | 4 +- homeassistant/components/demo/camera.py | 6 +- homeassistant/components/doorbird/camera.py | 6 +- .../components/environment_canada/camera.py | 6 +- homeassistant/components/esphome/camera.py | 4 +- homeassistant/components/ezviz/camera.py | 4 +- homeassistant/components/familyhub/camera.py | 6 +- homeassistant/components/ffmpeg/camera.py | 5 +- homeassistant/components/foscam/camera.py | 6 +- homeassistant/components/generic/camera.py | 10 +- .../components/homekit/manifest.json | 3 +- .../components/homekit/type_cameras.py | 10 +- .../components/homekit_controller/camera.py | 10 +- homeassistant/components/hyperion/camera.py | 4 +- homeassistant/components/local_file/camera.py | 7 +- .../components/logi_circle/camera.py | 6 +- homeassistant/components/mjpeg/camera.py | 16 ++- homeassistant/components/mqtt/camera.py | 6 +- homeassistant/components/neato/camera.py | 4 +- homeassistant/components/nest/camera_sdm.py | 4 +- .../components/nest/legacy/camera.py | 6 +- homeassistant/components/netatmo/camera.py | 8 +- homeassistant/components/onvif/camera.py | 6 +- homeassistant/components/proxy/camera.py | 16 ++- homeassistant/components/push/camera.py | 6 +- homeassistant/components/qvr_pro/camera.py | 5 +- homeassistant/components/ring/camera.py | 6 +- homeassistant/components/rpi_camera/camera.py | 6 +- homeassistant/components/skybell/camera.py | 6 +- .../components/synology_dsm/camera.py | 4 +- homeassistant/components/uvc/camera.py | 6 +- homeassistant/components/verisure/camera.py | 4 +- homeassistant/components/vivotek/camera.py | 6 +- homeassistant/components/xeoma/camera.py | 6 +- homeassistant/components/xiaomi/camera.py | 6 +- homeassistant/components/yi/camera.py | 6 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/camera/common.py | 17 +++ .../{homekit => camera}/test_img_util.py | 30 ++++- tests/components/camera/test_init.py | 47 ++++++++ tests/components/homekit/common.py | 17 --- tests/components/homekit/test_type_cameras.py | 4 +- 53 files changed, 418 insertions(+), 113 deletions(-) rename homeassistant/components/{homekit => camera}/img_util.py (72%) rename tests/components/{homekit => camera}/test_img_util.py (67%) delete mode 100644 tests/components/homekit/common.py diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index 99d4fd433a7..987e32f9911 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -1,4 +1,6 @@ """Support for Abode Security System cameras.""" +from __future__ import annotations + from datetime import timedelta import abodepy.helpers.constants as CONST @@ -73,7 +75,9 @@ class AbodeCamera(AbodeDevice, Camera): else: self._response = None - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Get a camera image.""" self.refresh_image() diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index 5c7f8acf94a..1478c658d18 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -1,4 +1,6 @@ """Support for Amcrest IP cameras.""" +from __future__ import annotations + import asyncio from datetime import timedelta from functools import partial @@ -181,7 +183,9 @@ class AmcrestCam(Camera): finally: self._snapshot_task = None - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" _LOGGER.debug("Take snapshot from %s", self._name) try: diff --git a/homeassistant/components/arlo/camera.py b/homeassistant/components/arlo/camera.py index 87c6216e56d..6b14f0cee0c 100644 --- a/homeassistant/components/arlo/camera.py +++ b/homeassistant/components/arlo/camera.py @@ -1,4 +1,6 @@ """Support for Netgear Arlo IP cameras.""" +from __future__ import annotations + import logging from haffmpeg.camera import CameraMjpeg @@ -62,7 +64,9 @@ class ArloCam(Camera): self._last_refresh = None self.attrs = {} - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" return self._camera.last_image_from_cache diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index 6bb47a06eee..6f9ecf1b182 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -1,4 +1,5 @@ """Support for August doorbell camera.""" +from __future__ import annotations from yalexs.activity import ActivityType from yalexs.util import update_doorbell_image_from_activity @@ -68,7 +69,9 @@ class AugustCamera(AugustEntityMixin, Camera): if doorbell_activity is not None: update_doorbell_image_from_activity(self._detail, doorbell_activity) - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return bytes of camera image.""" self._update_from_data() diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index e2216dc8785..8b4f1ba4eec 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -1,4 +1,6 @@ """Support for Blink system camera.""" +from __future__ import annotations + import logging from homeassistant.components.camera import Camera @@ -65,6 +67,8 @@ class BlinkCamera(Camera): self._camera.snap_picture() self.data.refresh() - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" return self._camera.image_from_cache.content diff --git a/homeassistant/components/bloomsky/camera.py b/homeassistant/components/bloomsky/camera.py index 570842b9c66..a7255a74d4c 100644 --- a/homeassistant/components/bloomsky/camera.py +++ b/homeassistant/components/bloomsky/camera.py @@ -1,4 +1,6 @@ """Support for a camera of a BloomSky weather station.""" +from __future__ import annotations + import logging import requests @@ -37,7 +39,9 @@ class BloomSkyCamera(Camera): self._logger = logging.getLogger(__name__) self._attr_unique_id = self._id - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Update the camera's image if it has changed.""" try: self._url = self._bloomsky.devices[self._id]["Data"]["ImageURL"] diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py index 34f1f173319..91e4bcffb17 100644 --- a/homeassistant/components/buienradar/camera.py +++ b/homeassistant/components/buienradar/camera.py @@ -143,7 +143,9 @@ class BuienradarCam(Camera): _LOGGER.error("Failed to fetch image, %s", type(err)) return False - async def async_camera_image(self) -> bytes | None: + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """ Return a still image response from the camera. diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index d1f354cc78e..c6cada2e3c9 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -8,7 +8,9 @@ from collections.abc import Awaitable, Mapping from contextlib import suppress from dataclasses import dataclass from datetime import datetime, timedelta +from functools import partial import hashlib +import inspect import logging import os from random import SystemRandom @@ -62,6 +64,7 @@ from .const import ( DOMAIN, SERVICE_RECORD, ) +from .img_util import scale_jpeg_camera_image from .prefs import CameraPreferences # mypy: allow-untyped-calls @@ -138,23 +141,72 @@ async def async_request_stream(hass: HomeAssistant, entity_id: str, fmt: str) -> return await _async_stream_endpoint_url(hass, camera, fmt) -@bind_hass -async def async_get_image( - hass: HomeAssistant, entity_id: str, timeout: int = 10 +async def _async_get_image( + camera: Camera, + timeout: int = 10, + width: int | None = None, + height: int | None = None, ) -> Image: - """Fetch an image from a camera entity.""" - camera = _get_camera_from_entity_id(hass, entity_id) + """Fetch a snapshot image from a camera. + If width and height are passed, an attempt to scale + the image will be made on a best effort basis. + Not all cameras can scale images or return jpegs + that we can scale, however the majority of cases + are handled. + """ with suppress(asyncio.CancelledError, asyncio.TimeoutError): async with async_timeout.timeout(timeout): - image = await camera.async_camera_image() + # Calling inspect will be removed in 2022.1 after all + # custom components have had a chance to change their signature + sig = inspect.signature(camera.async_camera_image) + if "height" in sig.parameters and "width" in sig.parameters: + image_bytes = await camera.async_camera_image( + width=width, height=height + ) + else: + _LOGGER.warning( + "The camera entity %s does not support requesting width and height, please open an issue with the integration author", + camera.entity_id, + ) + image_bytes = await camera.async_camera_image() - if image: - return Image(camera.content_type, image) + if image_bytes: + content_type = camera.content_type + image = Image(content_type, image_bytes) + if ( + width is not None + and height is not None + and "jpeg" in content_type + or "jpg" in content_type + ): + assert width is not None + assert height is not None + return Image( + content_type, scale_jpeg_camera_image(image, width, height) + ) + + return image raise HomeAssistantError("Unable to get image") +@bind_hass +async def async_get_image( + hass: HomeAssistant, + entity_id: str, + timeout: int = 10, + width: int | None = None, + height: int | None = None, +) -> Image: + """Fetch an image from a camera entity. + + width and height will be passed to the underlying camera. + """ + camera = _get_camera_from_entity_id(hass, entity_id) + return await _async_get_image(camera, timeout, width, height) + + @bind_hass async def async_get_stream_source(hass: HomeAssistant, entity_id: str) -> str | None: """Fetch the stream source for a camera entity.""" @@ -387,12 +439,27 @@ class Camera(Entity): """Return the source of the stream.""" return None - def camera_image(self) -> bytes | None: + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return bytes of camera image.""" raise NotImplementedError() - async def async_camera_image(self) -> bytes | None: + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return bytes of camera image.""" + sig = inspect.signature(self.camera_image) + # Calling inspect will be removed in 2022.1 after all + # custom components have had a chance to change their signature + if "height" in sig.parameters and "width" in sig.parameters: + return await self.hass.async_add_executor_job( + partial(self.camera_image, width=width, height=height) + ) + _LOGGER.warning( + "The camera entity %s does not support requesting width and height, please open an issue with the integration author", + self.entity_id, + ) return await self.hass.async_add_executor_job(self.camera_image) async def handle_async_still_stream( @@ -529,14 +596,19 @@ class CameraImageView(CameraView): async def handle(self, request: web.Request, camera: Camera) -> web.Response: """Serve camera image.""" - with suppress(asyncio.CancelledError, asyncio.TimeoutError): - async with async_timeout.timeout(CAMERA_IMAGE_TIMEOUT): - image = await camera.async_camera_image() - - if image: - return web.Response(body=image, content_type=camera.content_type) - - raise web.HTTPInternalServerError() + width = request.query.get("width") + height = request.query.get("height") + try: + image = await _async_get_image( + camera, + CAMERA_IMAGE_TIMEOUT, + int(width) if width else None, + int(height) if height else None, + ) + except (HomeAssistantError, ValueError) as ex: + raise web.HTTPInternalServerError() from ex + else: + return web.Response(body=image.content, content_type=image.content_type) class CameraMjpegStream(CameraView): diff --git a/homeassistant/components/homekit/img_util.py b/homeassistant/components/camera/img_util.py similarity index 72% rename from homeassistant/components/homekit/img_util.py rename to homeassistant/components/camera/img_util.py index 7d7a45081a6..4cfb4fda278 100644 --- a/homeassistant/components/homekit/img_util.py +++ b/homeassistant/components/camera/img_util.py @@ -1,19 +1,32 @@ -"""Image processing for HomeKit component.""" +"""Image processing for cameras.""" import logging +from typing import TYPE_CHECKING, cast SUPPORTED_SCALING_FACTORS = [(7, 8), (3, 4), (5, 8), (1, 2), (3, 8), (1, 4), (1, 8)] _LOGGER = logging.getLogger(__name__) +JPEG_QUALITY = 75 -def scale_jpeg_camera_image(cam_image, width, height): +if TYPE_CHECKING: + from turbojpeg import TurboJPEG + + from . import Image + + +def scale_jpeg_camera_image(cam_image: "Image", width: int, height: int) -> bytes: """Scale a camera image as close as possible to one of the supported scaling factors.""" turbo_jpeg = TurboJPEGSingleton.instance() if not turbo_jpeg: return cam_image.content - (current_width, current_height, _, _) = turbo_jpeg.decode_header(cam_image.content) + try: + (current_width, current_height, _, _) = turbo_jpeg.decode_header( + cam_image.content + ) + except OSError: + return cam_image.content if current_width <= width or current_height <= height: return cam_image.content @@ -26,10 +39,13 @@ def scale_jpeg_camera_image(cam_image, width, height): scaling_factor = supported_sf break - return turbo_jpeg.scale_with_quality( - cam_image.content, - scaling_factor=scaling_factor, - quality=75, + return cast( + bytes, + turbo_jpeg.scale_with_quality( + cam_image.content, + scaling_factor=scaling_factor, + quality=JPEG_QUALITY, + ), ) @@ -45,13 +61,13 @@ class TurboJPEGSingleton: __instance = None @staticmethod - def instance(): + def instance() -> "TurboJPEG": """Singleton for TurboJPEG.""" if TurboJPEGSingleton.__instance is None: TurboJPEGSingleton() return TurboJPEGSingleton.__instance - def __init__(self): + def __init__(self) -> None: """Try to create TurboJPEG only once.""" # pylint: disable=unused-private-member # https://github.com/PyCQA/pylint/issues/4681 diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index ed8e10c1956..6a27999c7fe 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -3,6 +3,7 @@ "name": "Camera", "documentation": "https://www.home-assistant.io/integrations/camera", "dependencies": ["http"], + "requirements": ["PyTurboJPEG==1.5.0"], "after_dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal" diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 2699ba1f640..7a2d22c2406 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -123,7 +123,9 @@ class CanaryCamera(CoordinatorEntity, Camera): """Return the camera motion detection status.""" return not self.location.is_recording - async def async_camera_image(self) -> bytes | None: + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" await self.hass.async_add_executor_job(self.renew_live_stream_session) live_stream_url = await self.hass.async_add_executor_job( diff --git a/homeassistant/components/demo/camera.py b/homeassistant/components/demo/camera.py index 56726bba8b7..b3f9b505aee 100644 --- a/homeassistant/components/demo/camera.py +++ b/homeassistant/components/demo/camera.py @@ -1,4 +1,6 @@ """Demo camera platform that has a fake camera.""" +from __future__ import annotations + from pathlib import Path from homeassistant.components.camera import SUPPORT_ON_OFF, Camera @@ -25,7 +27,9 @@ class DemoCamera(Camera): self.is_streaming = True self._images_index = 0 - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes: """Return a faked still image response.""" self._images_index = (self._images_index + 1) % 4 image_path = Path(__file__).parent / f"demo_{self._images_index}.jpg" diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index 53fcdbcee70..16606156314 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -1,4 +1,6 @@ """Support for viewing the camera feed from a DoorBird video doorbell.""" +from __future__ import annotations + import asyncio import datetime import logging @@ -112,7 +114,9 @@ class DoorBirdCamera(DoorBirdEntity, Camera): """Get the name of the camera.""" return self._name - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Pull a still image from the camera.""" now = dt_util.utcnow() diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py index 019dcb1aee5..ecd0c562d16 100644 --- a/homeassistant/components/environment_canada/camera.py +++ b/homeassistant/components/environment_canada/camera.py @@ -1,4 +1,6 @@ """Support for the Environment Canada radar imagery.""" +from __future__ import annotations + import datetime from env_canada import ECRadar @@ -68,7 +70,9 @@ class ECCamera(Camera): self.image = None self.timestamp = None - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return bytes of camera image.""" self.update() return self.image diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index 938d78362f7..47010324290 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -50,7 +50,9 @@ class EsphomeCamera(Camera, EsphomeEntity[CameraInfo, CameraState]): async with self._image_cond: self._image_cond.notify_all() - async def async_camera_image(self) -> bytes | None: + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return single camera image bytes.""" if not self.available: return None diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 76fbaee3757..4e5fdb90c79 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -325,7 +325,9 @@ class EzvizCamera(CoordinatorEntity, Camera): """Return the name of this camera.""" return self._serial - async def async_camera_image(self) -> bytes | None: + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a frame from the camera stream.""" ffmpeg = ImageFrame(self._ffmpeg.binary) diff --git a/homeassistant/components/familyhub/camera.py b/homeassistant/components/familyhub/camera.py index ea654074a5a..65b7a63e419 100644 --- a/homeassistant/components/familyhub/camera.py +++ b/homeassistant/components/familyhub/camera.py @@ -1,4 +1,6 @@ """Family Hub camera for Samsung Refrigerators.""" +from __future__ import annotations + from pyfamilyhublocal import FamilyHubCam import voluptuous as vol @@ -38,7 +40,9 @@ class FamilyHubCamera(Camera): self._name = name self.family_hub_cam = family_hub_cam - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response.""" return await self.family_hub_cam.async_get_cam_image() diff --git a/homeassistant/components/ffmpeg/camera.py b/homeassistant/components/ffmpeg/camera.py index 4cd8b0d1453..323eae7c129 100644 --- a/homeassistant/components/ffmpeg/camera.py +++ b/homeassistant/components/ffmpeg/camera.py @@ -1,4 +1,5 @@ """Support for Cameras with FFmpeg as decoder.""" +from __future__ import annotations from haffmpeg.camera import CameraMjpeg from haffmpeg.tools import IMAGE_JPEG @@ -49,7 +50,9 @@ class FFmpegCamera(Camera): """Return the stream source.""" return self._input.split(" ")[-1] - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" return await async_get_image( self.hass, diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index 31ac8c2cad9..7a1e1037ddb 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -1,4 +1,6 @@ """This component provides basic support for Foscam IP cameras.""" +from __future__ import annotations + import asyncio from libpyfoscam import FoscamCamera @@ -172,7 +174,9 @@ class HassFoscamCamera(Camera): """Return the entity unique ID.""" return self._unique_id - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" # Send the request to snap a picture and return raw jpg data # Handle exception if host is not reachable or url failed diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 56b490e165a..b6e08ea8582 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -1,4 +1,6 @@ """Support for IP Cameras.""" +from __future__ import annotations + import asyncio import logging @@ -118,13 +120,17 @@ class GenericCamera(Camera): """Return the interval between frames of the mjpeg stream.""" return self._frame_interval - def camera_image(self): + 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): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" try: url = self._still_image_url.async_render(parse_result=False) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 187d730de2f..c63ce2a8927 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -6,8 +6,7 @@ "HAP-python==4.0.0", "fnvhash==0.1.0", "PyQRCode==1.2.1", - "base36==0.1.1", - "PyTurboJPEG==1.5.0" + "base36==0.1.1" ], "dependencies": ["http", "camera", "ffmpeg", "network"], "after_dependencies": ["zeroconf"], diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index 077366870e2..4a8999ede08 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -55,7 +55,6 @@ from .const import ( SERV_SPEAKER, SERV_STATELESS_PROGRAMMABLE_SWITCH, ) -from .img_util import scale_jpeg_camera_image from .util import pid_is_alive _LOGGER = logging.getLogger(__name__) @@ -467,8 +466,9 @@ class Camera(HomeAccessory, PyhapCamera): async def async_get_snapshot(self, image_size): """Return a jpeg of a snapshot from the camera.""" - return scale_jpeg_camera_image( - await self.hass.components.camera.async_get_image(self.entity_id), - image_size["image-width"], - image_size["image-height"], + image = await self.hass.components.camera.async_get_image( + self.entity_id, + width=image_size["image-width"], + height=image_size["image-height"], ) + return image.content diff --git a/homeassistant/components/homekit_controller/camera.py b/homeassistant/components/homekit_controller/camera.py index fc6a5bb4522..a0b15087356 100644 --- a/homeassistant/components/homekit_controller/camera.py +++ b/homeassistant/components/homekit_controller/camera.py @@ -1,4 +1,6 @@ """Support for Homekit cameras.""" +from __future__ import annotations + from aiohomekit.model.services import ServicesTypes from homeassistant.components.camera import Camera @@ -21,12 +23,14 @@ class HomeKitCamera(AccessoryEntity, Camera): """Return the current state of the camera.""" return "idle" - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a jpeg with the current camera snapshot.""" return await self._accessory.pairing.image( self._aid, - 640, - 480, + width or 640, + height or 480, ) diff --git a/homeassistant/components/hyperion/camera.py b/homeassistant/components/hyperion/camera.py index 22134400a45..809449543af 100644 --- a/homeassistant/components/hyperion/camera.py +++ b/homeassistant/components/hyperion/camera.py @@ -210,7 +210,9 @@ class HyperionCamera(Camera): finally: await self._stop_image_streaming_for_client() - async def async_camera_image(self) -> bytes | None: + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return single camera image bytes.""" async with self._image_streaming() as is_streaming: if is_streaming: diff --git a/homeassistant/components/local_file/camera.py b/homeassistant/components/local_file/camera.py index 86a075c1a14..6e665ccd1c2 100644 --- a/homeassistant/components/local_file/camera.py +++ b/homeassistant/components/local_file/camera.py @@ -1,4 +1,6 @@ """Camera that loads a picture from a local file.""" +from __future__ import annotations + import logging import mimetypes import os @@ -73,7 +75,9 @@ class LocalFile(Camera): if content is not None: self.content_type = content - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return image response.""" try: with open(self._file_path, "rb") as file: @@ -84,6 +88,7 @@ class LocalFile(Camera): self._name, self._file_path, ) + return None def check_file_path_access(self, file_path): """Check that filepath given is readable.""" diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py index 1afeb190c8b..30407f03ecf 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -1,4 +1,6 @@ """Support to the Logi Circle cameras.""" +from __future__ import annotations + from datetime import timedelta import logging @@ -142,7 +144,9 @@ class LogiCam(Camera): return state - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image from the camera.""" return await self._camera.live_stream.download_jpeg() diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py index d5008d1778c..d486f78d334 100644 --- a/homeassistant/components/mjpeg/camera.py +++ b/homeassistant/components/mjpeg/camera.py @@ -1,4 +1,6 @@ """Support for IP Cameras.""" +from __future__ import annotations + import asyncio from contextlib import closing import logging @@ -106,7 +108,9 @@ class MjpegCamera(Camera): self._auth = aiohttp.BasicAuth(self._username, password=self._password) self._verify_ssl = device_info.get(CONF_VERIFY_SSL) - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" # DigestAuth is not supported if ( @@ -130,11 +134,17 @@ class MjpegCamera(Camera): except aiohttp.ClientError as err: _LOGGER.error("Error getting new camera image from %s: %s", self._name, err) - def camera_image(self): + return None + + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" if self._username and self._password: if self._authentication == HTTP_DIGEST_AUTHENTICATION: - auth = HTTPDigestAuth(self._username, self._password) + auth: HTTPDigestAuth | HTTPBasicAuth = HTTPDigestAuth( + self._username, self._password + ) else: auth = HTTPBasicAuth(self._username, self._password) req = requests.get( diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index adcb9ca623a..ebd6956e8fd 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -1,4 +1,6 @@ """Camera that loads a picture from an MQTT topic.""" +from __future__ import annotations + import functools import voluptuous as vol @@ -98,6 +100,8 @@ class MqttCamera(MqttEntity, Camera): }, ) - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return image response.""" return self._last_image diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index b6def2cfe38..392d586068d 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -66,7 +66,9 @@ class NeatoCleaningMap(Camera): self._image_url: str | None = None self._image: bytes | None = None - def camera_image(self) -> bytes | None: + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return image response.""" self.update() return self._image diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 5f5fdbc8d93..242c6147201 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -180,7 +180,9 @@ class NestCamera(Camera): self._device.add_update_listener(self.async_write_ha_state) ) - async def async_camera_image(self) -> bytes | None: + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return bytes of camera image.""" # Returns the snapshot of the last event for ~30 seconds after the event active_event_image = await self._async_active_event_image() diff --git a/homeassistant/components/nest/legacy/camera.py b/homeassistant/components/nest/legacy/camera.py index 77629e4dcff..3ef0089d2bc 100644 --- a/homeassistant/components/nest/legacy/camera.py +++ b/homeassistant/components/nest/legacy/camera.py @@ -1,4 +1,6 @@ """Support for Nest Cameras.""" +from __future__ import annotations + from datetime import timedelta import logging @@ -131,7 +133,9 @@ class NestCamera(Camera): def _ready_for_snapshot(self, now): return self._next_snapshot_at is None or now > self._next_snapshot_at - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" now = utcnow() if self._ready_for_snapshot(now): diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 32d0eb46286..4d6141e2dfb 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -194,10 +194,14 @@ class NetatmoCamera(NetatmoBase, Camera): self.data_handler.data[self._data_classes[0]["name"]], ) - async def async_camera_image(self) -> bytes | None: + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" try: - return await self._data.async_get_live_snapshot(camera_id=self._id) + return cast( + bytes, await self._data.async_get_live_snapshot(camera_id=self._id) + ) except ( aiohttp.ClientPayloadError, aiohttp.ContentTypeError, diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 0e95d24ef78..4d80231df23 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -1,4 +1,6 @@ """Support for ONVIF Cameras with FFmpeg as decoder.""" +from __future__ import annotations + import asyncio from haffmpeg.camera import CameraMjpeg @@ -120,7 +122,9 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): """Return the stream source.""" return self._stream_uri - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" image = None diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index 8fda507ace2..3c296b7d164 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -1,4 +1,6 @@ """Proxy camera platform that enables image processing of camera data.""" +from __future__ import annotations + import asyncio from datetime import timedelta import io @@ -219,13 +221,17 @@ class ProxyCamera(Camera): self._last_image = None self._mode = config.get(CONF_MODE) - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return camera image.""" return asyncio.run_coroutine_threadsafe( self.async_camera_image(), self.hass.loop ).result() - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" now = dt_util.utcnow() @@ -244,13 +250,13 @@ class ProxyCamera(Camera): job = _resize_image else: job = _crop_image - image = await self.hass.async_add_executor_job( + image_bytes: bytes = await self.hass.async_add_executor_job( job, image.content, self._image_opts ) if self._cache_images: - self._last_image = image - return image + self._last_image = image_bytes + return image_bytes async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from camera images.""" diff --git a/homeassistant/components/push/camera.py b/homeassistant/components/push/camera.py index ff0ac45c139..8f4d1d04dcf 100644 --- a/homeassistant/components/push/camera.py +++ b/homeassistant/components/push/camera.py @@ -1,4 +1,6 @@ """Camera platform that receives images through HTTP POST.""" +from __future__ import annotations + import asyncio from collections import deque from datetime import timedelta @@ -155,7 +157,9 @@ class PushCamera(Camera): self.async_write_ha_state() - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response.""" if self.queue: if self._state == STATE_IDLE: diff --git a/homeassistant/components/qvr_pro/camera.py b/homeassistant/components/qvr_pro/camera.py index 2f4353063d1..cac288eaef0 100644 --- a/homeassistant/components/qvr_pro/camera.py +++ b/homeassistant/components/qvr_pro/camera.py @@ -1,4 +1,5 @@ """Support for QVR Pro streams.""" +from __future__ import annotations import logging @@ -88,7 +89,9 @@ class QVRProCamera(Camera): return attrs - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Get image bytes from camera.""" try: return self._client.get_snapshot(self.guid) diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 580fc71e141..77317d62ab3 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -1,4 +1,6 @@ """This component provides support to the Ring Door Bell camera.""" +from __future__ import annotations + import asyncio from datetime import timedelta from itertools import chain @@ -101,7 +103,9 @@ class RingCam(RingEntityMixin, Camera): "last_video_id": self._last_video_id, } - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" ffmpeg = ImageFrame(self._ffmpeg.binary) diff --git a/homeassistant/components/rpi_camera/camera.py b/homeassistant/components/rpi_camera/camera.py index 070e861b3c9..980586d4def 100644 --- a/homeassistant/components/rpi_camera/camera.py +++ b/homeassistant/components/rpi_camera/camera.py @@ -1,4 +1,6 @@ """Camera platform that has a Raspberry Pi camera.""" +from __future__ import annotations + import logging import os import shutil @@ -122,7 +124,9 @@ class RaspberryCamera(Camera): ): pass - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return raspistill image response.""" with open(self._config[CONF_FILE_PATH], "rb") as file: return file.read() diff --git a/homeassistant/components/skybell/camera.py b/homeassistant/components/skybell/camera.py index 87dc3c0bf8d..20e93fb90f7 100644 --- a/homeassistant/components/skybell/camera.py +++ b/homeassistant/components/skybell/camera.py @@ -1,4 +1,6 @@ """Camera support for the Skybell HD Doorbell.""" +from __future__ import annotations + from datetime import timedelta import logging @@ -75,7 +77,9 @@ class SkybellCamera(SkybellDevice, Camera): return self._device.activity_image return self._device.image - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Get the latest camera image.""" super().update() diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index 8341b8b121a..d609a434ae2 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -123,7 +123,9 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): """Return the camera motion detection status.""" return self.camera_data.is_motion_detection_enabled # type: ignore[no-any-return] - def camera_image(self) -> bytes | None: + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return bytes of camera image.""" _LOGGER.debug( "SynoDSMCamera.camera_image(%s)", diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index 74bf175f75a..77ff6a30f95 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -194,10 +194,12 @@ class UnifiVideoCamera(Camera): self._caminfo = caminfo return True - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return the image of this camera.""" if not self._camera and not self._login(): - return + return None def _get_image(retry=True): try: diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index a137f61d98f..455d7070a8b 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -79,7 +79,9 @@ class VerisureSmartcam(CoordinatorEntity, Camera): "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]), } - def camera_image(self) -> bytes | None: + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return image response.""" self.check_imagelist() if not self._image: diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py index 953d64f0ff6..b813d337e82 100644 --- a/homeassistant/components/vivotek/camera.py +++ b/homeassistant/components/vivotek/camera.py @@ -1,4 +1,6 @@ """Support for Vivotek IP Cameras.""" +from __future__ import annotations + from libpyvivotek import VivotekCamera import voluptuous as vol @@ -87,7 +89,9 @@ class VivotekCam(Camera): """Return the interval between frames of the mjpeg stream.""" return self._frame_interval - def camera_image(self): + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return bytes of camera image.""" return self._cam.snapshot() diff --git a/homeassistant/components/xeoma/camera.py b/homeassistant/components/xeoma/camera.py index d6f313c0382..049b4bfcbc0 100644 --- a/homeassistant/components/xeoma/camera.py +++ b/homeassistant/components/xeoma/camera.py @@ -1,4 +1,6 @@ """Support for Xeoma Cameras.""" +from __future__ import annotations + import logging from pyxeoma.xeoma import Xeoma, XeomaError @@ -109,7 +111,9 @@ class XeomaCamera(Camera): self._password = password self._last_image = None - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" try: diff --git a/homeassistant/components/xiaomi/camera.py b/homeassistant/components/xiaomi/camera.py index 359d6c8b896..c89b23e9081 100644 --- a/homeassistant/components/xiaomi/camera.py +++ b/homeassistant/components/xiaomi/camera.py @@ -1,4 +1,6 @@ """This component provides support for Xiaomi Cameras.""" +from __future__ import annotations + import asyncio from ftplib import FTP, error_perm import logging @@ -138,7 +140,9 @@ class XiaomiCamera(Camera): return f"ftp://{self.user}:{self.passwd}@{host}:{self.port}{ftp.pwd()}/{video}" - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" try: diff --git a/homeassistant/components/yi/camera.py b/homeassistant/components/yi/camera.py index c130532a2e1..6f898bb9a9b 100644 --- a/homeassistant/components/yi/camera.py +++ b/homeassistant/components/yi/camera.py @@ -1,4 +1,6 @@ """Support for Xiaomi Cameras (HiSilicon Hi3518e V200).""" +from __future__ import annotations + import asyncio import logging @@ -119,7 +121,9 @@ class YiCamera(Camera): self._is_on = False return None - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" url = await self._get_latest_video_url() if url and url != self._last_url: diff --git a/requirements_all.txt b/requirements_all.txt index 813f40acf54..901ac8fd3b8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -57,7 +57,7 @@ PySocks==1.7.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 -# homeassistant.components.homekit +# homeassistant.components.camera PyTurboJPEG==1.5.0 # homeassistant.components.vicare diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4bc6cb76018..37ee164cd60 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -26,7 +26,7 @@ PyRMVtransport==0.3.2 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 -# homeassistant.components.homekit +# homeassistant.components.camera PyTurboJPEG==1.5.0 # homeassistant.components.xiaomi_aqara diff --git a/tests/components/camera/common.py b/tests/components/camera/common.py index 93e2596e343..756a553f3c7 100644 --- a/tests/components/camera/common.py +++ b/tests/components/camera/common.py @@ -3,8 +3,12 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ +from unittest.mock import Mock + from homeassistant.components.camera.const import DATA_CAMERA_PREFS, PREF_PRELOAD_STREAM +EMPTY_8_6_JPEG = b"empty_8_6" + def mock_camera_prefs(hass, entity_id, prefs=None): """Fixture for cloud component.""" @@ -13,3 +17,16 @@ def mock_camera_prefs(hass, entity_id, prefs=None): prefs_to_set.update(prefs) hass.data[DATA_CAMERA_PREFS]._prefs[entity_id] = prefs_to_set return prefs_to_set + + +def mock_turbo_jpeg( + first_width=None, second_width=None, first_height=None, second_height=None +): + """Mock a TurboJPEG instance.""" + mocked_turbo_jpeg = Mock() + mocked_turbo_jpeg.decode_header.side_effect = [ + (first_width, first_height, 0, 0), + (second_width, second_height, 0, 0), + ] + mocked_turbo_jpeg.scale_with_quality.return_value = EMPTY_8_6_JPEG + return mocked_turbo_jpeg diff --git a/tests/components/homekit/test_img_util.py b/tests/components/camera/test_img_util.py similarity index 67% rename from tests/components/homekit/test_img_util.py rename to tests/components/camera/test_img_util.py index 45af8e6b1e6..4f32715800e 100644 --- a/tests/components/homekit/test_img_util.py +++ b/tests/components/camera/test_img_util.py @@ -1,8 +1,10 @@ -"""Test HomeKit img_util module.""" +"""Test img_util module.""" from unittest.mock import patch +from turbojpeg import TurboJPEG + from homeassistant.components.camera import Image -from homeassistant.components.homekit.img_util import ( +from homeassistant.components.camera.img_util import ( TurboJPEGSingleton, scale_jpeg_camera_image, ) @@ -12,13 +14,23 @@ from .common import EMPTY_8_6_JPEG, mock_turbo_jpeg EMPTY_16_12_JPEG = b"empty_16_12" +def _clear_turbojpeg_singleton(): + TurboJPEGSingleton.__instance = None + + +def _reset_turbojpeg_singleton(): + TurboJPEGSingleton.__instance = TurboJPEG() + + def test_turbojpeg_singleton(): """Verify the instance always gives back the same.""" + _clear_turbojpeg_singleton() assert TurboJPEGSingleton.instance() == TurboJPEGSingleton.instance() def test_scale_jpeg_camera_image(): """Test we can scale a jpeg image.""" + _clear_turbojpeg_singleton() camera_image = Image("image/jpeg", EMPTY_16_12_JPEG) @@ -27,6 +39,12 @@ def test_scale_jpeg_camera_image(): TurboJPEGSingleton() assert scale_jpeg_camera_image(camera_image, 16, 12) == camera_image.content + turbo_jpeg = mock_turbo_jpeg(first_width=16, first_height=12) + turbo_jpeg.decode_header.side_effect = OSError + with patch("turbojpeg.TurboJPEG", return_value=turbo_jpeg): + TurboJPEGSingleton() + assert scale_jpeg_camera_image(camera_image, 16, 12) == camera_image.content + turbo_jpeg = mock_turbo_jpeg(first_width=16, first_height=12) with patch("turbojpeg.TurboJPEG", return_value=turbo_jpeg): TurboJPEGSingleton() @@ -44,11 +62,11 @@ def test_scale_jpeg_camera_image(): def test_turbojpeg_load_failure(): """Handle libjpegturbo not being installed.""" - + _clear_turbojpeg_singleton() with patch("turbojpeg.TurboJPEG", side_effect=Exception): TurboJPEGSingleton() assert TurboJPEGSingleton.instance() is False - with patch("turbojpeg.TurboJPEG"): - TurboJPEGSingleton() - assert TurboJPEGSingleton.instance() + _clear_turbojpeg_singleton() + TurboJPEGSingleton() + assert TurboJPEGSingleton.instance() is not None diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 7c7890a3e5f..f4267de234c 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -20,6 +20,8 @@ from homeassistant.const import ( from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component +from .common import EMPTY_8_6_JPEG, mock_turbo_jpeg + from tests.components.camera import common @@ -75,6 +77,51 @@ async def test_get_image_from_camera(hass, image_mock_url): assert image.content == b"Test" +async def test_get_image_from_camera_with_width_height(hass, image_mock_url): + """Grab an image from camera entity with width and height.""" + + turbo_jpeg = mock_turbo_jpeg( + first_width=16, first_height=12, second_width=300, second_height=200 + ) + with patch( + "homeassistant.components.camera.img_util.TurboJPEGSingleton.instance", + return_value=turbo_jpeg, + ), patch( + "homeassistant.components.demo.camera.Path.read_bytes", + autospec=True, + return_value=b"Test", + ) as mock_camera: + image = await camera.async_get_image( + hass, "camera.demo_camera", width=640, height=480 + ) + + assert mock_camera.called + assert image.content == b"Test" + + +async def test_get_image_from_camera_with_width_height_scaled(hass, image_mock_url): + """Grab an image from camera entity with width and height and scale it.""" + + turbo_jpeg = mock_turbo_jpeg( + first_width=16, first_height=12, second_width=300, second_height=200 + ) + with patch( + "homeassistant.components.camera.img_util.TurboJPEGSingleton.instance", + return_value=turbo_jpeg, + ), patch( + "homeassistant.components.demo.camera.Path.read_bytes", + autospec=True, + return_value=b"Valid jpeg", + ) as mock_camera: + image = await camera.async_get_image( + hass, "camera.demo_camera", width=4, height=3 + ) + + assert mock_camera.called + assert image.content_type == "image/jpeg" + assert image.content == EMPTY_8_6_JPEG + + async def test_get_stream_source_from_camera(hass, mock_camera): """Fetch stream source from camera entity.""" diff --git a/tests/components/homekit/common.py b/tests/components/homekit/common.py deleted file mode 100644 index 6b1d87e3f54..00000000000 --- a/tests/components/homekit/common.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Collection of fixtures and functions for the HomeKit tests.""" -from unittest.mock import Mock - -EMPTY_8_6_JPEG = b"empty_8_6" - - -def mock_turbo_jpeg( - first_width=None, second_width=None, first_height=None, second_height=None -): - """Mock a TurboJPEG instance.""" - mocked_turbo_jpeg = Mock() - mocked_turbo_jpeg.decode_header.side_effect = [ - (first_width, first_height, 0, 0), - (second_width, second_height, 0, 0), - ] - mocked_turbo_jpeg.scale_with_quality.return_value = EMPTY_8_6_JPEG - return mocked_turbo_jpeg diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index b9df572a699..991965b30b5 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -7,6 +7,7 @@ from pyhap.accessory_driver import AccessoryDriver import pytest from homeassistant.components import camera, ffmpeg +from homeassistant.components.camera.img_util import TurboJPEGSingleton from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( AUDIO_CODEC_COPY, @@ -26,14 +27,13 @@ from homeassistant.components.homekit.const import ( VIDEO_CODEC_COPY, VIDEO_CODEC_H264_OMX, ) -from homeassistant.components.homekit.img_util import TurboJPEGSingleton from homeassistant.components.homekit.type_cameras import Camera from homeassistant.components.homekit.type_switches import Switch from homeassistant.const import ATTR_DEVICE_CLASS, STATE_OFF, STATE_ON from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -from .common import mock_turbo_jpeg +from tests.components.camera.common import mock_turbo_jpeg MOCK_START_STREAM_TLV = "ARUCAQEBEDMD1QMXzEaatnKSQ2pxovYCNAEBAAIJAQECAgECAwEAAwsBAgAFAgLQAgMBHgQXAQFjAgQ768/RAwIrAQQEAAAAPwUCYgUDLAEBAwIMAQEBAgEAAwECBAEUAxYBAW4CBCzq28sDAhgABAQAAKBABgENBAEA" MOCK_END_POINTS_TLV = "ARAzA9UDF8xGmrZykkNqcaL2AgEAAxoBAQACDTE5Mi4xNjguMjA4LjUDAi7IBAKkxwQlAQEAAhDN0+Y0tZ4jzoO0ske9UsjpAw6D76oVXnoi7DbawIG4CwUlAQEAAhCyGcROB8P7vFRDzNF2xrK1Aw6NdcLugju9yCfkWVSaVAYEDoAsAAcEpxV8AA==" From d0b11568cc056c667f70ee6ca44cdf63e15f6de0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 10 Aug 2021 20:28:01 -0500 Subject: [PATCH 139/355] Ensure HomeKit passes min/max mireds as ints (#54372) --- .../components/homekit/type_lights.py | 5 +++-- tests/components/homekit/test_type_lights.py | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index aea760534fd..90c55d52153 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -1,5 +1,6 @@ """Class to hold all light accessories.""" import logging +import math from pyhap.const import CATEGORY_LIGHTBULB @@ -89,8 +90,8 @@ class Light(HomeAccessory): self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100) if self.color_temp_supported: - min_mireds = attributes.get(ATTR_MIN_MIREDS, 153) - max_mireds = attributes.get(ATTR_MAX_MIREDS, 500) + min_mireds = math.floor(attributes.get(ATTR_MIN_MIREDS, 153)) + max_mireds = math.ceil(attributes.get(ATTR_MAX_MIREDS, 500)) self.char_color_temp = serv_light.configure_char( CHAR_COLOR_TEMPERATURE, value=min_mireds, diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 90e3aa0cabe..de0fd532ec9 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -15,6 +15,8 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + ATTR_MAX_MIREDS, + ATTR_MIN_MIREDS, ATTR_SUPPORTED_COLOR_MODES, DOMAIN, ) @@ -639,6 +641,26 @@ async def test_light_set_brightness_and_color(hass, hk_driver, events): ) +async def test_light_min_max_mireds(hass, hk_driver, events): + """Test mireds are forced to ints.""" + entity_id = "light.demo" + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_COLOR_MODES: ["color_temp"], + ATTR_BRIGHTNESS: 255, + ATTR_MAX_MIREDS: 500.5, + ATTR_MIN_MIREDS: 100.5, + }, + ) + await hass.async_block_till_done() + acc = Light(hass, hk_driver, "Light", entity_id, 1, None) + acc.char_color_temp.properties["maxValue"] == 500 + acc.char_color_temp.properties["minValue"] == 100 + + async def test_light_set_brightness_and_color_temp(hass, hk_driver, events): """Test light with all chars in one go.""" entity_id = "light.demo" From 4d40d958488ffe40148ab6fd7a702c1da5b2fca6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 10 Aug 2021 20:31:11 -0500 Subject: [PATCH 140/355] Add support for width and height to ffmpeg based camera snapshots (#53837) --- homeassistant/components/canary/camera.py | 18 +++---- homeassistant/components/ezviz/camera.py | 12 ++--- homeassistant/components/ffmpeg/__init__.py | 12 +++++ homeassistant/components/onvif/camera.py | 19 +++---- homeassistant/components/ring/camera.py | 21 +++----- homeassistant/components/xiaomi/camera.py | 14 ++--- homeassistant/components/yi/camera.py | 14 ++--- tests/components/ffmpeg/test_init.py | 59 ++++++++++++++++++++- 8 files changed, 109 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 7a2d22c2406..a475a27f942 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -1,7 +1,6 @@ """Support for Canary camera.""" from __future__ import annotations -import asyncio from datetime import timedelta from typing import Final @@ -9,9 +8,9 @@ from aiohttp.web import Request, StreamResponse from canary.api import Device, Location from canary.live_stream_api import LiveStreamSession from haffmpeg.camera import CameraMjpeg -from haffmpeg.tools import IMAGE_JPEG, ImageFrame import voluptuous as vol +from homeassistant.components import ffmpeg from homeassistant.components.camera import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, Camera, @@ -131,16 +130,13 @@ class CanaryCamera(CoordinatorEntity, Camera): live_stream_url = await self.hass.async_add_executor_job( getattr, self._live_stream_session, "live_stream_url" ) - - ffmpeg = ImageFrame(self._ffmpeg.binary) - image: bytes | None = await asyncio.shield( - ffmpeg.get_image( - live_stream_url, - output_format=IMAGE_JPEG, - extra_cmd=self._ffmpeg_arguments, - ) + return await ffmpeg.async_get_image( + self.hass, + live_stream_url, + extra_cmd=self._ffmpeg_arguments, + width=width, + height=height, ) - return image async def handle_async_mjpeg_stream( self, request: Request diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 4e5fdb90c79..44a90e2928f 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -1,13 +1,12 @@ """Support ezviz camera devices.""" from __future__ import annotations -import asyncio import logging -from haffmpeg.tools import IMAGE_JPEG, ImageFrame from pyezviz.exceptions import HTTPError, InvalidHost, PyEzvizError 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.config_entries import ( @@ -329,12 +328,11 @@ class EzvizCamera(CoordinatorEntity, Camera): self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return a frame from the camera stream.""" - ffmpeg = ImageFrame(self._ffmpeg.binary) - - image = await asyncio.shield( - ffmpeg.get_image(self._rtsp_stream, output_format=IMAGE_JPEG) + if self._rtsp_stream is None: + return None + return await ffmpeg.async_get_image( + self.hass, self._rtsp_stream, width=width, height=height ) - return image @property def device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index 52e034c6265..74c826f47d6 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.loader import bind_hass DOMAIN = "ffmpeg" @@ -89,15 +90,26 @@ async def async_setup(hass, config): return True +@bind_hass async def async_get_image( hass: HomeAssistant, input_source: str, output_format: str = IMAGE_JPEG, extra_cmd: str | None = None, + width: int | None = None, + height: int | None = None, ) -> bytes | None: """Get an image from a frame of an RTSP stream.""" manager = hass.data[DATA_FFMPEG] ffmpeg = ImageFrame(manager.binary) + + if width and height and (extra_cmd is None or "-s" not in extra_cmd): + size_cmd = f"-s {width}x{height}" + if extra_cmd is None: + extra_cmd = size_cmd + else: + extra_cmd += " " + size_cmd + image = await asyncio.shield( ffmpeg.get_image(input_source, output_format=output_format, extra_cmd=extra_cmd) ) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 4d80231df23..bb7cffa86f9 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -1,14 +1,12 @@ """Support for ONVIF Cameras with FFmpeg as decoder.""" from __future__ import annotations -import asyncio - from haffmpeg.camera import CameraMjpeg -from haffmpeg.tools import IMAGE_JPEG, ImageFrame from onvif.exceptions import ONVIFError import voluptuous as vol 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.const import HTTP_BASIC_AUTHENTICATION @@ -141,15 +139,12 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): ) if image is None: - ffmpeg = ImageFrame(self.hass.data[DATA_FFMPEG].binary) - image = await asyncio.shield( - ffmpeg.get_image( - self._stream_uri, - output_format=IMAGE_JPEG, - extra_cmd=self.device.config_entry.options.get( - CONF_EXTRA_ARGUMENTS - ), - ) + return await ffmpeg.async_get_image( + self.hass, + self._stream_uri, + extra_cmd=self.device.config_entry.options.get(CONF_EXTRA_ARGUMENTS), + width=width, + height=height, ) return image diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 77317d62ab3..509877ae5ff 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -1,15 +1,14 @@ """This component provides support to the Ring Door Bell camera.""" from __future__ import annotations -import asyncio from datetime import timedelta from itertools import chain import logging from haffmpeg.camera import CameraMjpeg -from haffmpeg.tools import IMAGE_JPEG, ImageFrame import requests +from homeassistant.components import ffmpeg from homeassistant.components.camera import Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ATTR_ATTRIBUTION @@ -44,12 +43,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class RingCam(RingEntityMixin, Camera): """An implementation of a Ring Door Bell camera.""" - def __init__(self, config_entry_id, ffmpeg, device): + def __init__(self, config_entry_id, ffmpeg_manager, device): """Initialize a Ring Door Bell camera.""" super().__init__(config_entry_id, device) self._name = self._device.name - self._ffmpeg = ffmpeg + self._ffmpeg_manager = ffmpeg_manager self._last_event = None self._last_video_id = None self._video_url = None @@ -107,25 +106,19 @@ class RingCam(RingEntityMixin, Camera): self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return a still image response from the camera.""" - ffmpeg = ImageFrame(self._ffmpeg.binary) - if self._video_url is None: return - image = await asyncio.shield( - ffmpeg.get_image( - self._video_url, - output_format=IMAGE_JPEG, - ) + return await ffmpeg.async_get_image( + self.hass, self._video_url, width=width, height=height ) - return image async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" if self._video_url is None: return - stream = CameraMjpeg(self._ffmpeg.binary) + stream = CameraMjpeg(self._ffmpeg_manager.binary) await stream.open_camera(self._video_url) try: @@ -134,7 +127,7 @@ class RingCam(RingEntityMixin, Camera): self.hass, request, stream_reader, - self._ffmpeg.ffmpeg_stream_content_type, + self._ffmpeg_manager.ffmpeg_stream_content_type, ) finally: await stream.close() diff --git a/homeassistant/components/xiaomi/camera.py b/homeassistant/components/xiaomi/camera.py index c89b23e9081..016fe7dd2ba 100644 --- a/homeassistant/components/xiaomi/camera.py +++ b/homeassistant/components/xiaomi/camera.py @@ -1,14 +1,13 @@ """This component provides support for Xiaomi Cameras.""" from __future__ import annotations -import asyncio from ftplib import FTP, error_perm import logging from haffmpeg.camera import CameraMjpeg -from haffmpeg.tools import IMAGE_JPEG, ImageFrame 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.const import ( @@ -153,11 +152,12 @@ class XiaomiCamera(Camera): url = await self.hass.async_add_executor_job(self.get_latest_video_url, host) if url != self._last_url: - ffmpeg = ImageFrame(self._manager.binary) - self._last_image = await asyncio.shield( - ffmpeg.get_image( - url, output_format=IMAGE_JPEG, extra_cmd=self._extra_arguments - ) + self._last_image = await ffmpeg.async_get_image( + self.hass, + url, + extra_cmd=self._extra_arguments, + width=width, + height=height, ) self._last_url = url diff --git a/homeassistant/components/yi/camera.py b/homeassistant/components/yi/camera.py index 6f898bb9a9b..91dfaab38bf 100644 --- a/homeassistant/components/yi/camera.py +++ b/homeassistant/components/yi/camera.py @@ -1,14 +1,13 @@ """Support for Xiaomi Cameras (HiSilicon Hi3518e V200).""" from __future__ import annotations -import asyncio import logging from aioftp import Client, StatusCodeError from haffmpeg.camera import CameraMjpeg -from haffmpeg.tools import IMAGE_JPEG, ImageFrame 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.const import ( @@ -127,11 +126,12 @@ class YiCamera(Camera): """Return a still image response from the camera.""" url = await self._get_latest_video_url() if url and url != self._last_url: - ffmpeg = ImageFrame(self._manager.binary) - self._last_image = await asyncio.shield( - ffmpeg.get_image( - url, output_format=IMAGE_JPEG, extra_cmd=self._extra_arguments - ), + self._last_image = await ffmpeg.async_get_image( + self.hass, + url, + extra_cmd=self._extra_arguments, + width=width, + height=height, ) self._last_url = url diff --git a/tests/components/ffmpeg/test_init.py b/tests/components/ffmpeg/test_init.py index 3c6a2fbb92d..e1730ffdabb 100644 --- a/tests/components/ffmpeg/test_init.py +++ b/tests/components/ffmpeg/test_init.py @@ -1,7 +1,7 @@ """The tests for Home Assistant ffmpeg.""" -from unittest.mock import MagicMock +from unittest.mock import AsyncMock, MagicMock, Mock, call, patch -import homeassistant.components.ffmpeg as ffmpeg +from homeassistant.components import ffmpeg from homeassistant.components.ffmpeg import ( DOMAIN, SERVICE_RESTART, @@ -181,3 +181,58 @@ async def test_setup_component_test_service_start_with_entity(hass): assert ffmpeg_dev.called_start assert ffmpeg_dev.called_entities == ["test.ffmpeg_device"] + + +async def test_async_get_image_with_width_height(hass): + """Test fetching an image with a specific width and height.""" + with assert_setup_component(1): + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + + get_image_mock = AsyncMock() + with patch( + "homeassistant.components.ffmpeg.ImageFrame", + return_value=Mock(get_image=get_image_mock), + ): + await ffmpeg.async_get_image(hass, "rtsp://fake", width=640, height=480) + + assert get_image_mock.call_args_list == [ + call("rtsp://fake", output_format="mjpeg", extra_cmd="-s 640x480") + ] + + +async def test_async_get_image_with_extra_cmd_overlapping_width_height(hass): + """Test fetching an image with and extra_cmd with width and height and a specific width and height.""" + with assert_setup_component(1): + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + + get_image_mock = AsyncMock() + with patch( + "homeassistant.components.ffmpeg.ImageFrame", + return_value=Mock(get_image=get_image_mock), + ): + await ffmpeg.async_get_image( + hass, "rtsp://fake", extra_cmd="-s 1024x768", width=640, height=480 + ) + + assert get_image_mock.call_args_list == [ + call("rtsp://fake", output_format="mjpeg", extra_cmd="-s 1024x768") + ] + + +async def test_async_get_image_with_extra_cmd_width_height(hass): + """Test fetching an image with and extra_cmd and a specific width and height.""" + with assert_setup_component(1): + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + + get_image_mock = AsyncMock() + with patch( + "homeassistant.components.ffmpeg.ImageFrame", + return_value=Mock(get_image=get_image_mock), + ): + await ffmpeg.async_get_image( + hass, "rtsp://fake", extra_cmd="-vf any", width=640, height=480 + ) + + assert get_image_mock.call_args_list == [ + call("rtsp://fake", output_format="mjpeg", extra_cmd="-vf any -s 640x480") + ] From 10551743d6f7eb8549339f5c68ef642a5c97d17e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 10 Aug 2021 20:28:03 -0700 Subject: [PATCH 141/355] Bump pyopenuv to 2.1.0 (#54436) --- homeassistant/components/openuv/__init__.py | 2 +- homeassistant/components/openuv/config_flow.py | 2 +- homeassistant/components/openuv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index efe6fa89ca8..bcdd0b2ba40 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -59,8 +59,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b config_entry.data[CONF_API_KEY], config_entry.data.get(CONF_LATITUDE, hass.config.latitude), config_entry.data.get(CONF_LONGITUDE, hass.config.longitude), - websession, altitude=config_entry.data.get(CONF_ELEVATION, hass.config.elevation), + session=websession, ) ) await openuv.async_update() diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index 54b2aca0b75..3595b124053 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -71,7 +71,7 @@ class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() websession = aiohttp_client.async_get_clientsession(self.hass) - client = Client(user_input[CONF_API_KEY], 0, 0, websession) + client = Client(user_input[CONF_API_KEY], 0, 0, session=websession) try: await client.uv_index() diff --git a/homeassistant/components/openuv/manifest.json b/homeassistant/components/openuv/manifest.json index 81e38d251f1..842d4966805 100644 --- a/homeassistant/components/openuv/manifest.json +++ b/homeassistant/components/openuv/manifest.json @@ -3,7 +3,7 @@ "name": "OpenUV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/openuv", - "requirements": ["pyopenuv==1.0.9"], + "requirements": ["pyopenuv==2.1.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 901ac8fd3b8..dfcd9d694fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1649,7 +1649,7 @@ pyobihai==1.3.1 pyombi==0.1.10 # homeassistant.components.openuv -pyopenuv==1.0.9 +pyopenuv==2.1.0 # homeassistant.components.opnsense pyopnsense==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 37ee164cd60..638bd10520f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -941,7 +941,7 @@ pynx584==0.5 pynzbgetapi==0.2.0 # homeassistant.components.openuv -pyopenuv==1.0.9 +pyopenuv==2.1.0 # homeassistant.components.opnsense pyopnsense==0.2.0 From adcbd8b115908042b3fcd34708acb69d4c2be22f Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 11 Aug 2021 08:31:52 +0200 Subject: [PATCH 142/355] =?UTF-8?q?Activate=20mypy=20for=20Tr=C3=A5dfri=20?= =?UTF-8?q?(#54416)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Activate mypy. --- homeassistant/components/tradfri/__init__.py | 8 ++++++-- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index cf39d3d6c05..8508dab5b96 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -1,6 +1,9 @@ """Support for IKEA Tradfri.""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import Any from pytradfri import Gateway, RequestError from pytradfri.api.aiocoap_api import APIFactory @@ -70,7 +73,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType): load_json, hass.config.path(CONFIG_FILE) ) - for host, info in legacy_hosts.items(): + for host, info in legacy_hosts.items(): # type: ignore if host in configured_hosts: continue @@ -103,7 +106,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Create a gateway.""" # host, identity, key, allow_tradfri_groups - tradfri_data = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {} + tradfri_data: dict[str, Any] = {} + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = tradfri_data listeners = tradfri_data[LISTENERS] = [] factory = await APIFactory.init( diff --git a/mypy.ini b/mypy.ini index 6fffe2bc3c1..0cd2a419fe0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1691,9 +1691,6 @@ ignore_errors = true [mypy-homeassistant.components.tplink.*] ignore_errors = true -[mypy-homeassistant.components.tradfri.*] -ignore_errors = true - [mypy-homeassistant.components.tuya.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 9747a5ee8c0..188f2a0a41b 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -156,7 +156,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.todoist.*", "homeassistant.components.toon.*", "homeassistant.components.tplink.*", - "homeassistant.components.tradfri.*", "homeassistant.components.tuya.*", "homeassistant.components.unifi.*", "homeassistant.components.upnp.*", From 480fd53b4b4640e362aeb7b883dbd6727954aea2 Mon Sep 17 00:00:00 2001 From: Brett Date: Wed, 11 Aug 2021 17:49:31 +1000 Subject: [PATCH 143/355] Advantage Air code cleanup (#54449) --- homeassistant/components/advantage_air/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index eca7651d6eb..f6d59129603 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -112,7 +112,7 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone wireless signal sensor.""" - super().__init__(instance, ac_key, zone_key=zone_key) + super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} Signal' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-signal' @@ -149,7 +149,9 @@ class AdvantageAirZoneTemp(AdvantageAirEntity, SensorEntity): """Initialize an Advantage Air Zone Temp Sensor.""" super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} Temperature' - self._attr_unique_id = f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-temp' + self._attr_unique_id = ( + f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-temp' + ) @property def state(self): From 930c1dbe9bd7bbb18bd4bfc4129fd330000eb6a9 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 11 Aug 2021 00:58:53 -0700 Subject: [PATCH 144/355] Bump aioambient to 1.2.6 (#54442) --- homeassistant/components/ambient_station/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json index 42b22d26a10..35b4770e872 100644 --- a/homeassistant/components/ambient_station/manifest.json +++ b/homeassistant/components/ambient_station/manifest.json @@ -3,7 +3,7 @@ "name": "Ambient Weather Station", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambient_station", - "requirements": ["aioambient==1.2.5"], + "requirements": ["aioambient==1.2.6"], "codeowners": ["@bachya"], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index dfcd9d694fb..8fe9b1c6d57 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -139,7 +139,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.5 # homeassistant.components.ambient_station -aioambient==1.2.5 +aioambient==1.2.6 # homeassistant.components.asuswrt aioasuswrt==1.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 638bd10520f..67caf01a0e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -78,7 +78,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.5 # homeassistant.components.ambient_station -aioambient==1.2.5 +aioambient==1.2.6 # homeassistant.components.asuswrt aioasuswrt==1.3.4 From 4e07ab1b323724b2aaae7af10eee9ac4ca7bb531 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Aug 2021 10:45:05 +0200 Subject: [PATCH 145/355] Move temperature conversions to sensor base class (1/8) (#48261) * Move temperature conversions to entity base class (1/8) * Update integrations a-c * Leave old temperature conversion until all integrations are migrated * tweak * Use contextlib.suppress * Remove the MeasurableUnitEntity mixin * Address comments, add tests * Fix f-string * Drop deprecation warning from base entity class * Update with _attr-shorthand * Fix rebase mistakes * Fix additional rebase mistakes * Only report temperature conversion once * Fix additional rebase mistakes * Format homeassistant/components/bbox/sensor.py * Fix check for overidden _attr_state * Remove test workarounds from implementation * Remove useless None-check * Tweaks * Migrate new sensors a-c * Update climacell * Push deprecation of temperature conversion forward * Override __repr__ in SensorEntity * Include native_value in SensorEntity attributes * Pylint * Black * Black * Fix rebase mistakes * black * Fix rebase mistakes * Revert changes in august/sensor.py * Revert handling of unit converted restored state * Apply code review suggestion * Fix arlo test --- homeassistant/components/abode/sensor.py | 8 +- .../components/accuweather/sensor.py | 6 +- homeassistant/components/acmeda/sensor.py | 2 +- homeassistant/components/adguard/sensor.py | 4 +- homeassistant/components/ads/sensor.py | 4 +- .../components/advantage_air/sensor.py | 14 +-- homeassistant/components/aemet/sensor.py | 6 +- homeassistant/components/aftership/sensor.py | 4 +- homeassistant/components/airly/const.py | 14 +-- homeassistant/components/airly/sensor.py | 2 +- homeassistant/components/airnow/sensor.py | 4 +- homeassistant/components/airvisual/sensor.py | 36 ++++--- .../components/alarmdecoder/sensor.py | 4 +- .../components/alpha_vantage/sensor.py | 8 +- homeassistant/components/ambee/const.py | 46 ++++---- homeassistant/components/ambee/sensor.py | 2 +- .../components/ambient_station/sensor.py | 8 +- homeassistant/components/amcrest/sensor.py | 4 +- .../components/android_ip_webcam/sensor.py | 4 +- homeassistant/components/apcupsd/sensor.py | 10 +- homeassistant/components/aqualogic/sensor.py | 8 +- homeassistant/components/arduino/sensor.py | 2 +- homeassistant/components/arest/sensor.py | 6 +- homeassistant/components/arlo/sensor.py | 2 +- homeassistant/components/arwn/sensor.py | 4 +- homeassistant/components/asuswrt/sensor.py | 14 +-- homeassistant/components/atag/sensor.py | 6 +- homeassistant/components/atome/sensor.py | 8 +- homeassistant/components/august/sensor.py | 8 +- homeassistant/components/aurora/sensor.py | 4 +- .../components/aurora_abb_powerone/sensor.py | 6 +- homeassistant/components/awair/sensor.py | 4 +- .../components/azure_devops/sensor.py | 4 +- homeassistant/components/bbox/sensor.py | 22 ++-- .../components/beewi_smartclim/sensor.py | 10 +- homeassistant/components/bh1750/sensor.py | 4 +- homeassistant/components/bitcoin/sensor.py | 70 ++++++------ homeassistant/components/bizkaibus/sensor.py | 4 +- homeassistant/components/blebox/sensor.py | 4 +- homeassistant/components/blink/sensor.py | 6 +- homeassistant/components/blockchain/sensor.py | 4 +- homeassistant/components/bloomsky/sensor.py | 10 +- homeassistant/components/bme280/sensor.py | 4 +- homeassistant/components/bme680/sensor.py | 16 +-- homeassistant/components/bmp280/sensor.py | 6 +- .../components/bmw_connected_drive/sensor.py | 20 ++-- homeassistant/components/bosch_shc/sensor.py | 32 +++--- homeassistant/components/broadlink/sensor.py | 6 +- homeassistant/components/brother/const.py | 44 ++++---- homeassistant/components/brother/sensor.py | 2 +- .../components/brottsplatskartan/sensor.py | 2 +- homeassistant/components/buienradar/sensor.py | 28 ++--- homeassistant/components/canary/sensor.py | 2 +- .../components/cert_expiry/sensor.py | 2 +- homeassistant/components/citybikes/sensor.py | 4 +- homeassistant/components/climacell/sensor.py | 4 +- homeassistant/components/co2signal/sensor.py | 4 +- homeassistant/components/coinbase/sensor.py | 8 +- .../components/comed_hourly_pricing/sensor.py | 4 +- .../components/comfoconnect/sensor.py | 4 +- .../components/command_line/sensor.py | 4 +- .../components/compensation/sensor.py | 4 +- .../components/coronavirus/sensor.py | 4 +- homeassistant/components/cpuspeed/sensor.py | 4 +- homeassistant/components/cups/sensor.py | 8 +- .../components/currencylayer/sensor.py | 4 +- homeassistant/components/sensor/__init__.py | 102 +++++++++++++++++- homeassistant/helpers/entity.py | 35 +++--- tests/components/arlo/test_sensor.py | 76 ++++++------- tests/components/sensor/test_init.py | 30 ++++++ .../custom_components/test/sensor.py | 13 ++- 71 files changed, 516 insertions(+), 360 deletions(-) create mode 100644 tests/components/sensor/test_init.py diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index a0681e0440f..03687fc3907 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -61,14 +61,14 @@ class AbodeSensor(AbodeDevice, SensorEntity): self._attr_name = f"{device.name} {description.name}" self._attr_unique_id = f"{device.device_uuid}-{description.key}" if description.key == CONST.TEMP_STATUS_KEY: - self._attr_unit_of_measurement = device.temp_unit + self._attr_native_unit_of_measurement = device.temp_unit elif description.key == CONST.HUMI_STATUS_KEY: - self._attr_unit_of_measurement = device.humidity_unit + self._attr_native_unit_of_measurement = device.humidity_unit elif description.key == CONST.LUX_STATUS_KEY: - self._attr_unit_of_measurement = device.lux_unit + self._attr_native_unit_of_measurement = device.lux_unit @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.entity_description.key == CONST.TEMP_STATUS_KEY: return self._device.temp diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 4a5af6054e1..b5f979b45cf 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -88,10 +88,10 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): ) if coordinator.is_metric: self._unit_system = API_METRIC - self._attr_unit_of_measurement = description.unit_metric + self._attr_native_unit_of_measurement = description.unit_metric else: self._unit_system = API_IMPERIAL - self._attr_unit_of_measurement = description.unit_imperial + self._attr_native_unit_of_measurement = description.unit_imperial self._attr_device_info = { "identifiers": {(DOMAIN, coordinator.location_key)}, "name": NAME, @@ -101,7 +101,7 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): self.forecast_day = forecast_day @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state.""" if self.forecast_day is not None: if self.entity_description.device_class == DEVICE_CLASS_TEMPERATURE: diff --git a/homeassistant/components/acmeda/sensor.py b/homeassistant/components/acmeda/sensor.py index 4f617c5726f..7cded0adb30 100644 --- a/homeassistant/components/acmeda/sensor.py +++ b/homeassistant/components/acmeda/sensor.py @@ -42,6 +42,6 @@ class AcmedaBattery(AcmedaBase, SensorEntity): return f"{super().name} Battery" @property - def state(self): + def native_value(self): """Return the state of the device.""" return self.roller.battery diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index 7499cf51d0c..a7f4dabde9f 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -82,12 +82,12 @@ class AdGuardHomeSensor(AdGuardHomeDeviceEntity, SensorEntity): ) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit this state is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/ads/sensor.py b/homeassistant/components/ads/sensor.py index fe68c4c860b..26b04d86050 100644 --- a/homeassistant/components/ads/sensor.py +++ b/homeassistant/components/ads/sensor.py @@ -50,7 +50,7 @@ class AdsSensor(AdsEntity, SensorEntity): def __init__(self, ads_hub, ads_var, ads_type, name, unit_of_measurement, factor): """Initialize AdsSensor entity.""" super().__init__(ads_hub, name, ads_var) - self._attr_unit_of_measurement = unit_of_measurement + self._attr_native_unit_of_measurement = unit_of_measurement self._ads_type = ads_type self._factor = factor @@ -64,6 +64,6 @@ class AdsSensor(AdsEntity, SensorEntity): ) @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the device.""" return self._state_dict[STATE_KEY_STATE] diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index f6d59129603..65b7b35740e 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -45,7 +45,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air timer control.""" - _attr_unit_of_measurement = ADVANTAGE_AIR_SET_COUNTDOWN_UNIT + _attr_native_unit_of_measurement = ADVANTAGE_AIR_SET_COUNTDOWN_UNIT def __init__(self, instance, ac_key, action): """Initialize the Advantage Air timer control.""" @@ -58,7 +58,7 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): ) @property - def state(self): + def native_value(self): """Return the current value.""" return self._ac[self._time_key] @@ -78,7 +78,7 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air Zone Vent Sensor.""" - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE _attr_state_class = STATE_CLASS_MEASUREMENT def __init__(self, instance, ac_key, zone_key): @@ -90,7 +90,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): ) @property - def state(self): + def native_value(self): """Return the current value of the air vent.""" if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN: return self._zone["value"] @@ -107,7 +107,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air Zone wireless signal sensor.""" - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE _attr_state_class = STATE_CLASS_MEASUREMENT def __init__(self, instance, ac_key, zone_key): @@ -119,7 +119,7 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): ) @property - def state(self): + def native_value(self): """Return the current value of the wireless signal.""" return self._zone["rssi"] @@ -140,7 +140,7 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): class AdvantageAirZoneTemp(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air Zone wireless signal sensor.""" - _attr_unit_of_measurement = TEMP_CELSIUS + _attr_native_unit_of_measurement = TEMP_CELSIUS _attr_state_class = STATE_CLASS_MEASUREMENT _attr_icon = "mdi:thermometer" _attr_entity_registry_enabled_default = False diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index 3fd0769cb00..35336980e1a 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -85,7 +85,7 @@ class AbstractAemetSensor(CoordinatorEntity, SensorEntity): self._attr_name = f"{self._name} {self._sensor_name}" self._attr_unique_id = self._unique_id self._attr_device_class = sensor_configuration.get(SENSOR_DEVICE_CLASS) - self._attr_unit_of_measurement = sensor_configuration.get(SENSOR_UNIT) + self._attr_native_unit_of_measurement = sensor_configuration.get(SENSOR_UNIT) class AemetSensor(AbstractAemetSensor): @@ -106,7 +106,7 @@ class AemetSensor(AbstractAemetSensor): self._weather_coordinator = weather_coordinator @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._weather_coordinator.data.get(self._sensor_type) @@ -134,7 +134,7 @@ class AemetForecastSensor(AbstractAemetSensor): ) @property - def state(self): + def native_value(self): """Return the state of the device.""" forecast = None forecasts = self._weather_coordinator.data.get( diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index fd8e095f65f..a3b41f8314c 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -109,7 +109,7 @@ async def async_setup_platform( class AfterShipSensor(SensorEntity): """Representation of a AfterShip sensor.""" - _attr_unit_of_measurement: str = "packages" + _attr_native_unit_of_measurement: str = "packages" _attr_icon: str = ICON def __init__(self, aftership: Tracking, name: str) -> None: @@ -120,7 +120,7 @@ class AfterShipSensor(SensorEntity): self._attr_name = name @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index 157f28c33f7..e6b87db6f15 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -50,34 +50,34 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = ( AirlySensorEntityDescription( key=ATTR_API_CAQI, name=ATTR_API_CAQI, - unit_of_measurement="CAQI", + native_unit_of_measurement="CAQI", ), AirlySensorEntityDescription( key=ATTR_API_PM1, icon="mdi:blur", name=ATTR_API_PM1, - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), AirlySensorEntityDescription( key=ATTR_API_PM25, icon="mdi:blur", name="PM2.5", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), AirlySensorEntityDescription( key=ATTR_API_PM10, icon="mdi:blur", name=ATTR_API_PM10, - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), AirlySensorEntityDescription( key=ATTR_API_HUMIDITY, device_class=DEVICE_CLASS_HUMIDITY, name=ATTR_API_HUMIDITY.capitalize(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, value=lambda value: round(value, 1), ), @@ -85,14 +85,14 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = ( key=ATTR_API_PRESSURE, device_class=DEVICE_CLASS_PRESSURE, name=ATTR_API_PRESSURE.capitalize(), - unit_of_measurement=PRESSURE_HPA, + native_unit_of_measurement=PRESSURE_HPA, state_class=STATE_CLASS_MEASUREMENT, ), AirlySensorEntityDescription( key=ATTR_API_TEMPERATURE, device_class=DEVICE_CLASS_TEMPERATURE, name=ATTR_API_TEMPERATURE.capitalize(), - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, state_class=STATE_CLASS_MEASUREMENT, value=lambda value: round(value, 1), ), diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 2c811b00aa6..b5d45afd2d8 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -84,7 +84,7 @@ class AirlySensor(CoordinatorEntity, SensorEntity): self.entity_description = description @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state.""" state = self.coordinator.data[self.entity_description.key] return cast(StateType, self.entity_description.value(state)) diff --git a/homeassistant/components/airnow/sensor.py b/homeassistant/components/airnow/sensor.py index 31ea5e298e3..a0f8d7e701b 100644 --- a/homeassistant/components/airnow/sensor.py +++ b/homeassistant/components/airnow/sensor.py @@ -72,11 +72,11 @@ class AirNowSensor(CoordinatorEntity, SensorEntity): self._attr_name = f"AirNow {SENSOR_TYPES[self.kind][ATTR_LABEL]}" self._attr_icon = SENSOR_TYPES[self.kind][ATTR_ICON] self._attr_device_class = SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] - self._attr_unit_of_measurement = SENSOR_TYPES[self.kind][ATTR_UNIT] + self._attr_native_unit_of_measurement = SENSOR_TYPES[self.kind][ATTR_UNIT] self._attr_unique_id = f"{self.coordinator.latitude}-{self.coordinator.longitude}-{self.kind.lower()}" @property - def state(self): + def native_value(self): """Return the state.""" self._state = self.coordinator.data[self.kind] return self._state diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 2f8dd07c625..922c84357ae 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -212,7 +212,7 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity): self._attr_icon = icon self._attr_name = f"{GEOGRAPHY_SENSOR_LOCALES[locale]} {name}" self._attr_unique_id = f"{config_entry.unique_id}_{locale}_{kind}" - self._attr_unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit self._config_entry = config_entry self._kind = kind self._locale = locale @@ -232,16 +232,16 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity): if self._kind == SENSOR_KIND_LEVEL: aqi = data[f"aqi{self._locale}"] - [(self._attr_state, self._attr_icon)] = [ + [(self._attr_native_value, self._attr_icon)] = [ (name, icon) for (floor, ceiling), (name, icon) in POLLUTANT_LEVELS.items() if floor <= aqi <= ceiling ] elif self._kind == SENSOR_KIND_AQI: - self._attr_state = data[f"aqi{self._locale}"] + self._attr_native_value = data[f"aqi{self._locale}"] elif self._kind == SENSOR_KIND_POLLUTANT: symbol = data[f"main{self._locale}"] - self._attr_state = symbol + self._attr_native_value = symbol self._attr_extra_state_attributes.update( { ATTR_POLLUTANT_SYMBOL: symbol, @@ -298,7 +298,7 @@ class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): f"{coordinator.data['settings']['node_name']} Node/Pro: {name}" ) self._attr_unique_id = f"{coordinator.data['serial_number']}_{kind}" - self._attr_unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit self._kind = kind @property @@ -320,24 +320,30 @@ class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): """Update the entity from the latest data.""" if self._kind == SENSOR_KIND_AQI: if self.coordinator.data["settings"]["is_aqi_usa"]: - self._attr_state = self.coordinator.data["measurements"]["aqi_us"] + self._attr_native_value = self.coordinator.data["measurements"][ + "aqi_us" + ] else: - self._attr_state = self.coordinator.data["measurements"]["aqi_cn"] + self._attr_native_value = self.coordinator.data["measurements"][ + "aqi_cn" + ] elif self._kind == SENSOR_KIND_BATTERY_LEVEL: - self._attr_state = self.coordinator.data["status"]["battery"] + self._attr_native_value = self.coordinator.data["status"]["battery"] elif self._kind == SENSOR_KIND_CO2: - self._attr_state = self.coordinator.data["measurements"].get("co2") + self._attr_native_value = self.coordinator.data["measurements"].get("co2") elif self._kind == SENSOR_KIND_HUMIDITY: - self._attr_state = self.coordinator.data["measurements"].get("humidity") + self._attr_native_value = self.coordinator.data["measurements"].get( + "humidity" + ) elif self._kind == SENSOR_KIND_PM_0_1: - self._attr_state = self.coordinator.data["measurements"].get("pm0_1") + self._attr_native_value = self.coordinator.data["measurements"].get("pm0_1") elif self._kind == SENSOR_KIND_PM_1_0: - self._attr_state = self.coordinator.data["measurements"].get("pm1_0") + self._attr_native_value = self.coordinator.data["measurements"].get("pm1_0") elif self._kind == SENSOR_KIND_PM_2_5: - self._attr_state = self.coordinator.data["measurements"].get("pm2_5") + self._attr_native_value = self.coordinator.data["measurements"].get("pm2_5") elif self._kind == SENSOR_KIND_TEMPERATURE: - self._attr_state = self.coordinator.data["measurements"].get( + self._attr_native_value = self.coordinator.data["measurements"].get( "temperature_C" ) elif self._kind == SENSOR_KIND_VOC: - self._attr_state = self.coordinator.data["measurements"].get("voc") + self._attr_native_value = self.coordinator.data["measurements"].get("voc") diff --git a/homeassistant/components/alarmdecoder/sensor.py b/homeassistant/components/alarmdecoder/sensor.py index 67b7ee4861a..16471010ee9 100644 --- a/homeassistant/components/alarmdecoder/sensor.py +++ b/homeassistant/components/alarmdecoder/sensor.py @@ -32,6 +32,6 @@ class AlarmDecoderSensor(SensorEntity): ) def _message_callback(self, message): - if self._attr_state != message.text: - self._attr_state = message.text + if self._attr_native_value != message.text: + self._attr_native_value = message.text self.schedule_update_ha_state() diff --git a/homeassistant/components/alpha_vantage/sensor.py b/homeassistant/components/alpha_vantage/sensor.py index 512de247ff2..583485ca703 100644 --- a/homeassistant/components/alpha_vantage/sensor.py +++ b/homeassistant/components/alpha_vantage/sensor.py @@ -112,7 +112,7 @@ class AlphaVantageSensor(SensorEntity): self._symbol = symbol[CONF_SYMBOL] self._attr_name = symbol.get(CONF_NAME, self._symbol) self._timeseries = timeseries - self._attr_unit_of_measurement = symbol.get(CONF_CURRENCY, self._symbol) + self._attr_native_unit_of_measurement = symbol.get(CONF_CURRENCY, self._symbol) self._attr_icon = ICONS.get(symbol.get(CONF_CURRENCY, "USD")) def update(self): @@ -120,7 +120,7 @@ class AlphaVantageSensor(SensorEntity): _LOGGER.debug("Requesting new data for symbol %s", self._symbol) all_values, _ = self._timeseries.get_intraday(self._symbol) values = next(iter(all_values.values())) - self._attr_state = values["1. open"] + self._attr_native_value = values["1. open"] self._attr_extra_state_attributes = ( { ATTR_ATTRIBUTION: ATTRIBUTION, @@ -148,7 +148,7 @@ class AlphaVantageForeignExchange(SensorEntity): else f"{self._to_currency}/{self._from_currency}" ) self._attr_icon = ICONS.get(self._from_currency, "USD") - self._attr_unit_of_measurement = self._to_currency + self._attr_native_unit_of_measurement = self._to_currency def update(self): """Get the latest data and updates the states.""" @@ -160,7 +160,7 @@ class AlphaVantageForeignExchange(SensorEntity): values, _ = self._foreign_exchange.get_currency_exchange_rate( from_currency=self._from_currency, to_currency=self._to_currency ) - self._attr_state = round(float(values["5. Exchange Rate"]), 4) + self._attr_native_value = round(float(values["5. Exchange Rate"]), 4) self._attr_extra_state_attributes = ( { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/ambee/const.py b/homeassistant/components/ambee/const.py index d2570bea710..3fd57c17c63 100644 --- a/homeassistant/components/ambee/const.py +++ b/homeassistant/components/ambee/const.py @@ -39,38 +39,38 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { SensorEntityDescription( key="particulate_matter_2_5", name="Particulate Matter < 2.5 μm", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="particulate_matter_10", name="Particulate Matter < 10 μm", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="sulphur_dioxide", name="Sulphur Dioxide (SO2)", - unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="nitrogen_dioxide", name="Nitrogen Dioxide (NO2)", - unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="ozone", name="Ozone", - unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="carbon_monoxide", name="Carbon Monoxide (CO)", device_class=DEVICE_CLASS_CO, - unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( @@ -85,21 +85,21 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Grass Pollen", icon="mdi:grass", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, ), SensorEntityDescription( key="tree", name="Tree Pollen", icon="mdi:tree", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, ), SensorEntityDescription( key="weed", name="Weed Pollen", icon="mdi:sprout", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, ), SensorEntityDescription( key="grass_risk", @@ -124,7 +124,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Poaceae Grass Pollen", icon="mdi:grass", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -132,7 +132,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Alder Tree Pollen", icon="mdi:tree", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -140,7 +140,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Birch Tree Pollen", icon="mdi:tree", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -148,7 +148,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Cypress Tree Pollen", icon="mdi:tree", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -156,7 +156,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Elm Tree Pollen", icon="mdi:tree", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -164,7 +164,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Hazel Tree Pollen", icon="mdi:tree", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -172,7 +172,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Oak Tree Pollen", icon="mdi:tree", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -180,7 +180,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Pine Tree Pollen", icon="mdi:tree", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -188,7 +188,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Plane Tree Pollen", icon="mdi:tree", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -196,7 +196,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Poplar Tree Pollen", icon="mdi:tree", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -204,7 +204,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Chenopod Weed Pollen", icon="mdi:sprout", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -212,7 +212,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Mugwort Weed Pollen", icon="mdi:sprout", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -220,7 +220,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Nettle Weed Pollen", icon="mdi:sprout", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -228,7 +228,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { name="Ragweed Weed Pollen", icon="mdi:sprout", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), ], diff --git a/homeassistant/components/ambee/sensor.py b/homeassistant/components/ambee/sensor.py index ecd04ffd204..bd125ac973e 100644 --- a/homeassistant/components/ambee/sensor.py +++ b/homeassistant/components/ambee/sensor.py @@ -66,7 +66,7 @@ class AmbeeSensorEntity(CoordinatorEntity, SensorEntity): } @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the sensor.""" value = getattr(self.coordinator.data, self.entity_description.key) if isinstance(value, str): diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index a606b401bc0..935a53e9384 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -61,7 +61,7 @@ class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity): ambient, mac_address, station_name, sensor_type, sensor_name, device_class ) - self._attr_unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit @callback def update_from_latest_data(self) -> None: @@ -75,10 +75,10 @@ class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity): ].get(TYPE_SOLARRADIATION) if w_m2_brightness_val is None: - self._attr_state = None + self._attr_native_value = None else: - self._attr_state = round(float(w_m2_brightness_val) / 0.0079) + self._attr_native_value = round(float(w_m2_brightness_val) / 0.0079) else: - self._attr_state = self._ambient.stations[self._mac_address][ + self._attr_native_value = self._ambient.stations[self._mac_address][ ATTR_LAST_DATA ].get(self._sensor_type) diff --git a/homeassistant/components/amcrest/sensor.py b/homeassistant/components/amcrest/sensor.py index a30de62494e..de8370a15fc 100644 --- a/homeassistant/components/amcrest/sensor.py +++ b/homeassistant/components/amcrest/sensor.py @@ -61,7 +61,7 @@ class AmcrestSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -76,7 +76,7 @@ class AmcrestSensor(SensorEntity): return self._icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the units of measurement.""" return self._unit_of_measurement diff --git a/homeassistant/components/android_ip_webcam/sensor.py b/homeassistant/components/android_ip_webcam/sensor.py index adedb297cd1..4bef3848617 100644 --- a/homeassistant/components/android_ip_webcam/sensor.py +++ b/homeassistant/components/android_ip_webcam/sensor.py @@ -50,12 +50,12 @@ class IPWebcamSensor(AndroidIPCamEntity, SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index bf1b8bf6db5..5937ff6a852 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -165,16 +165,16 @@ class APCUPSdSensor(SensorEntity): self.type = sensor_type self._attr_name = SENSOR_PREFIX + SENSOR_TYPES[sensor_type][0] self._attr_icon = SENSOR_TYPES[self.type][2] - self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_native_unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._attr_device_class = SENSOR_TYPES[sensor_type][3] def update(self): """Get the latest status and use it to update our sensor state.""" if self.type.upper() not in self._data.status: - self._attr_state = None + self._attr_native_value = None else: - self._attr_state, inferred_unit = infer_unit( + self._attr_native_value, inferred_unit = infer_unit( self._data.status[self.type.upper()] ) - if not self._attr_unit_of_measurement: - self._attr_unit_of_measurement = inferred_unit + if not self._attr_native_unit_of_measurement: + self._attr_native_unit_of_measurement = inferred_unit diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py index fff73cf00fa..394f8844adb 100644 --- a/homeassistant/components/aqualogic/sensor.py +++ b/homeassistant/components/aqualogic/sensor.py @@ -92,11 +92,11 @@ class AquaLogicSensor(SensorEntity): panel = self._processor.panel if panel is not None: if panel.is_metric: - self._attr_unit_of_measurement = SENSOR_TYPES[self._type][1][0] + self._attr_native_unit_of_measurement = SENSOR_TYPES[self._type][1][0] else: - self._attr_unit_of_measurement = SENSOR_TYPES[self._type][1][1] + self._attr_native_unit_of_measurement = SENSOR_TYPES[self._type][1][1] - self._attr_state = getattr(panel, self._type) + self._attr_native_value = getattr(panel, self._type) self.async_write_ha_state() else: - self._attr_unit_of_measurement = None + self._attr_native_unit_of_measurement = None diff --git a/homeassistant/components/arduino/sensor.py b/homeassistant/components/arduino/sensor.py index fa624a7d167..0853fb5537d 100644 --- a/homeassistant/components/arduino/sensor.py +++ b/homeassistant/components/arduino/sensor.py @@ -42,4 +42,4 @@ class ArduinoSensor(SensorEntity): def update(self): """Get the latest value from the pin.""" - self._attr_state = self._board.get_analog_inputs()[self._pin][1] + self._attr_native_value = self._board.get_analog_inputs()[self._pin][1] diff --git a/homeassistant/components/arest/sensor.py b/homeassistant/components/arest/sensor.py index 7129b989f47..addd666e30e 100644 --- a/homeassistant/components/arest/sensor.py +++ b/homeassistant/components/arest/sensor.py @@ -141,7 +141,7 @@ class ArestSensor(SensorEntity): self.arest = arest self._attr_name = f"{location.title()} {name.title()}" self._variable = variable - self._attr_unit_of_measurement = unit_of_measurement + self._attr_native_unit_of_measurement = unit_of_measurement self._renderer = renderer if pin is not None: @@ -155,9 +155,9 @@ class ArestSensor(SensorEntity): self._attr_available = self.arest.available values = self.arest.data if "error" in values: - self._attr_state = values["error"] + self._attr_native_value = values["error"] else: - self._attr_state = self._renderer( + self._attr_native_value = self._renderer( values.get("value", values.get(self._variable, None)) ) diff --git a/homeassistant/components/arlo/sensor.py b/homeassistant/components/arlo/sensor.py index e78c8b7bf49..d17668ae721 100644 --- a/homeassistant/components/arlo/sensor.py +++ b/homeassistant/components/arlo/sensor.py @@ -127,7 +127,7 @@ class ArloSensor(SensorEntity): self.async_schedule_update_ha_state(True) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/arwn/sensor.py b/homeassistant/components/arwn/sensor.py index 2300319f9a4..321be5035cd 100644 --- a/homeassistant/components/arwn/sensor.py +++ b/homeassistant/components/arwn/sensor.py @@ -138,7 +138,7 @@ class ArwnSensor(SensorEntity): # This mqtt topic for the sensor which is its uid self._attr_unique_id = topic self._state_key = state_key - self._attr_unit_of_measurement = units + self._attr_native_unit_of_measurement = units self._attr_icon = icon self._attr_device_class = device_class @@ -147,5 +147,5 @@ class ArwnSensor(SensorEntity): ev = {} ev.update(event) self._attr_extra_state_attributes = ev - self._attr_state = ev.get(self._state_key, None) + self._attr_native_value = ev.get(self._state_key, None) self.async_write_ha_state() diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index ef186a80085..3367cc37ee4 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -48,13 +48,13 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( key=SENSORS_CONNECTED_DEVICE[0], name="Devices Connected", icon="mdi:router-network", - unit_of_measurement=UNIT_DEVICES, + native_unit_of_measurement=UNIT_DEVICES, ), AsusWrtSensorEntityDescription( key=SENSORS_RATES[0], name="Download Speed", icon="mdi:download-network", - unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, + native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, entity_registry_enabled_default=False, factor=125000, ), @@ -62,7 +62,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( key=SENSORS_RATES[1], name="Upload Speed", icon="mdi:upload-network", - unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, + native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, entity_registry_enabled_default=False, factor=125000, ), @@ -70,7 +70,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( key=SENSORS_BYTES[0], name="Download", icon="mdi:download", - unit_of_measurement=DATA_GIGABYTES, + native_unit_of_measurement=DATA_GIGABYTES, entity_registry_enabled_default=False, factor=1000000000, ), @@ -78,7 +78,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( key=SENSORS_BYTES[1], name="Upload", icon="mdi:upload", - unit_of_measurement=DATA_GIGABYTES, + native_unit_of_measurement=DATA_GIGABYTES, entity_registry_enabled_default=False, factor=1000000000, ), @@ -150,11 +150,11 @@ class AsusWrtSensor(CoordinatorEntity, SensorEntity): self._attr_unique_id = f"{DOMAIN} {self.name}" self._attr_state_class = STATE_CLASS_MEASUREMENT - if description.unit_of_measurement == DATA_GIGABYTES: + if description.native_unit_of_measurement == DATA_GIGABYTES: self._attr_last_reset = dt_util.utc_from_timestamp(0) @property - def state(self) -> str: + def native_value(self) -> str: """Return current state.""" descr = self.entity_description state = self.coordinator.data.get(descr.key) diff --git a/homeassistant/components/atag/sensor.py b/homeassistant/components/atag/sensor.py index 014c6cb463e..386b5999712 100644 --- a/homeassistant/components/atag/sensor.py +++ b/homeassistant/components/atag/sensor.py @@ -49,10 +49,12 @@ class AtagSensor(AtagEntity, SensorEntity): PERCENTAGE, TIME_HOURS, ): - self._attr_unit_of_measurement = coordinator.data.report[self._id].measure + self._attr_native_unit_of_measurement = coordinator.data.report[ + self._id + ].measure @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self.coordinator.data.report[self._id].state diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 7295a9cee41..498e760924a 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -258,10 +258,10 @@ class AtomeSensor(SensorEntity): if sensor_type == LIVE_TYPE: self._attr_device_class = DEVICE_CLASS_POWER - self._attr_unit_of_measurement = POWER_WATT + self._attr_native_unit_of_measurement = POWER_WATT else: self._attr_device_class = DEVICE_CLASS_ENERGY - self._attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR + self._attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR def update(self): """Update device state.""" @@ -269,13 +269,13 @@ class AtomeSensor(SensorEntity): update_function() if self._sensor_type == LIVE_TYPE: - self._attr_state = self._data.live_power + self._attr_native_value = self._data.live_power self._attr_extra_state_attributes = { "subscribed_power": self._data.subscribed_power, "is_connected": self._data.is_connected, } else: - self._attr_state = getattr(self._data, f"{self._sensor_type}_usage") + self._attr_native_value = getattr(self._data, f"{self._sensor_type}_usage") self._attr_last_reset = dt_util.as_utc( getattr(self._data, f"{self._sensor_type}_last_reset") ) diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index a174964f349..b6d93d3b3b1 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -146,7 +146,7 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity): self._attr_available = True if lock_activity is not None: - self._attr_state = lock_activity.operated_by + self._attr_native_value = lock_activity.operated_by self._operated_remote = lock_activity.operated_remote self._operated_keypad = lock_activity.operated_keypad self._operated_autorelock = lock_activity.operated_autorelock @@ -208,7 +208,7 @@ class AugustBatterySensor(AugustEntityMixin, SensorEntity): """Representation of an August sensor.""" _attr_device_class = DEVICE_CLASS_BATTERY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, data, sensor_type, device, old_device): """Initialize the sensor.""" @@ -223,8 +223,8 @@ class AugustBatterySensor(AugustEntityMixin, SensorEntity): def _update_from_data(self): """Get the latest state of the sensor.""" state_provider = SENSOR_TYPES_BATTERY[self._sensor_type]["state_provider"] - self._attr_state = state_provider(self._detail) - self._attr_available = self._attr_state is not None + self._attr_native_value = state_provider(self._detail) + self._attr_available = self._attr_native_value is not None @property def old_unique_id(self) -> str: diff --git a/homeassistant/components/aurora/sensor.py b/homeassistant/components/aurora/sensor.py index 76be6ca97f8..96bdbbf1370 100644 --- a/homeassistant/components/aurora/sensor.py +++ b/homeassistant/components/aurora/sensor.py @@ -22,9 +22,9 @@ async def async_setup_entry(hass, entry, async_add_entries): class AuroraSensor(AuroraEntity, SensorEntity): """Implementation of an aurora sensor.""" - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE @property - def state(self): + def native_value(self): """Return % chance the aurora is visible.""" return self.coordinator.data diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index 9c798b8e6d4..b1bcec18796 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -51,7 +51,7 @@ class AuroraABBSolarPVMonitorSensor(SensorEntity): """Representation of a Sensor.""" _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_unit_of_measurement = POWER_WATT + _attr_native_unit_of_measurement = POWER_WATT _attr_device_class = DEVICE_CLASS_POWER def __init__(self, client, name, typename): @@ -68,7 +68,7 @@ class AuroraABBSolarPVMonitorSensor(SensorEntity): self.client.connect() # read ADC channel 3 (grid power output) power_watts = self.client.measure(3, True) - self._attr_state = round(power_watts, 1) + self._attr_native_value = round(power_watts, 1) except AuroraError as error: # aurorapy does not have different exceptions (yet) for dealing # with timeout vs other comms errors. @@ -82,7 +82,7 @@ class AuroraABBSolarPVMonitorSensor(SensorEntity): _LOGGER.debug("No response from inverter (could be dark)") else: raise error - self._attr_state = None + self._attr_native_value = None finally: if self.client.serline.isOpen(): self.client.close() diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index 968587c3b10..3b46d3b2317 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -144,7 +144,7 @@ class AwairSensor(CoordinatorEntity, SensorEntity): return False @property - def state(self) -> float: + def native_value(self) -> float: """Return the state, rounding off to reasonable values.""" state: float @@ -175,7 +175,7 @@ class AwairSensor(CoordinatorEntity, SensorEntity): return SENSOR_TYPES[self._kind][ATTR_DEVICE_CLASS] @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit the value is expressed in.""" return SENSOR_TYPES[self._kind][ATTR_UNIT] diff --git a/homeassistant/components/azure_devops/sensor.py b/homeassistant/components/azure_devops/sensor.py index d7589cf5014..67d472abc1e 100644 --- a/homeassistant/components/azure_devops/sensor.py +++ b/homeassistant/components/azure_devops/sensor.py @@ -71,7 +71,7 @@ class AzureDevOpsSensor(AzureDevOpsDeviceEntity, SensorEntity): unit_of_measurement: str = "", ) -> None: """Initialize Azure DevOps sensor.""" - self._attr_unit_of_measurement = unit_of_measurement + self._attr_native_unit_of_measurement = unit_of_measurement self.client = client self.organization = organization self.project = project @@ -107,7 +107,7 @@ class AzureDevOpsLatestBuildSensor(AzureDevOpsSensor): _LOGGER.warning(exception) self._attr_available = False return False - self._attr_state = build.build_number + self._attr_native_value = build.build_number self._attr_extra_state_attributes = { "definition_id": build.definition.id, "definition_name": build.definition.name, diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index b0ace5fa675..9ccd197e05e 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -94,7 +94,7 @@ class BboxUptimeSensor(SensorEntity): def __init__(self, bbox_data, sensor_type, name): """Initialize the sensor.""" self._attr_name = f"{name} {SENSOR_TYPES[sensor_type][0]}" - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_native_unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._attr_icon = SENSOR_TYPES[sensor_type][2] self.bbox_data = bbox_data @@ -104,7 +104,7 @@ class BboxUptimeSensor(SensorEntity): uptime = utcnow() - timedelta( seconds=self.bbox_data.router_infos["device"]["uptime"] ) - self._attr_state = uptime.replace(microsecond=0).isoformat() + self._attr_native_value = uptime.replace(microsecond=0).isoformat() class BboxSensor(SensorEntity): @@ -116,7 +116,7 @@ class BboxSensor(SensorEntity): """Initialize the sensor.""" self.type = sensor_type self._attr_name = f"{name} {SENSOR_TYPES[sensor_type][0]}" - self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_native_unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._attr_icon = SENSOR_TYPES[sensor_type][2] self.bbox_data = bbox_data @@ -124,19 +124,25 @@ class BboxSensor(SensorEntity): """Get the latest data from Bbox and update the state.""" self.bbox_data.update() if self.type == "down_max_bandwidth": - self._attr_state = round( + self._attr_native_value = round( self.bbox_data.data["rx"]["maxBandwidth"] / 1000, 2 ) elif self.type == "up_max_bandwidth": - self._attr_state = round( + self._attr_native_value = round( self.bbox_data.data["tx"]["maxBandwidth"] / 1000, 2 ) elif self.type == "current_down_bandwidth": - self._attr_state = round(self.bbox_data.data["rx"]["bandwidth"] / 1000, 2) + self._attr_native_value = round( + self.bbox_data.data["rx"]["bandwidth"] / 1000, 2 + ) elif self.type == "current_up_bandwidth": - self._attr_state = round(self.bbox_data.data["tx"]["bandwidth"] / 1000, 2) + self._attr_native_value = round( + self.bbox_data.data["tx"]["bandwidth"] / 1000, 2 + ) elif self.type == "number_of_reboots": - self._attr_state = self.bbox_data.router_infos["device"]["numberofboots"] + self._attr_native_value = self.bbox_data.router_infos["device"][ + "numberofboots" + ] class BboxData: diff --git a/homeassistant/components/beewi_smartclim/sensor.py b/homeassistant/components/beewi_smartclim/sensor.py index 2ed6b71be41..9ec81956c56 100644 --- a/homeassistant/components/beewi_smartclim/sensor.py +++ b/homeassistant/components/beewi_smartclim/sensor.py @@ -63,17 +63,17 @@ class BeewiSmartclimSensor(SensorEntity): self._poller = poller self._attr_name = name self._device = device - self._attr_unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit self._attr_device_class = self._device self._attr_unique_id = f"{mac}_{device}" def update(self): """Fetch new state data from the poller.""" self._poller.update_sensor() - self._attr_state = None + self._attr_native_value = None if self._device == DEVICE_CLASS_TEMPERATURE: - self._attr_state = self._poller.get_temperature() + self._attr_native_value = self._poller.get_temperature() if self._device == DEVICE_CLASS_HUMIDITY: - self._attr_state = self._poller.get_humidity() + self._attr_native_value = self._poller.get_humidity() if self._device == DEVICE_CLASS_BATTERY: - self._attr_state = self._poller.get_battery() + self._attr_native_value = self._poller.get_battery() diff --git a/homeassistant/components/bh1750/sensor.py b/homeassistant/components/bh1750/sensor.py index 8a1f8c60ccf..ad5ca13684a 100644 --- a/homeassistant/components/bh1750/sensor.py +++ b/homeassistant/components/bh1750/sensor.py @@ -101,7 +101,7 @@ class BH1750Sensor(SensorEntity): def __init__(self, bh1750_sensor, name, unit, multiplier=1.0): """Initialize the sensor.""" self._attr_name = name - self._attr_unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit self._multiplier = multiplier self.bh1750_sensor = bh1750_sensor @@ -109,7 +109,7 @@ class BH1750Sensor(SensorEntity): """Get the latest data from the BH1750 and update the states.""" await self.hass.async_add_executor_job(self.bh1750_sensor.update) if self.bh1750_sensor.sample_ok and self.bh1750_sensor.light_level >= 0: - self._attr_state = int( + self._attr_native_value = int( round(self.bh1750_sensor.light_level * self._multiplier) ) else: diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index 29945bd56dc..b66f775eae2 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -39,22 +39,22 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="trade_volume_btc", name="Trade volume", - unit_of_measurement="BTC", + native_unit_of_measurement="BTC", ), SensorEntityDescription( key="miners_revenue_usd", name="Miners revenue", - unit_of_measurement="USD", + native_unit_of_measurement="USD", ), SensorEntityDescription( key="btc_mined", name="Mined", - unit_of_measurement="BTC", + native_unit_of_measurement="BTC", ), SensorEntityDescription( key="trade_volume_usd", name="Trade volume", - unit_of_measurement="USD", + native_unit_of_measurement="USD", ), SensorEntityDescription( key="difficulty", @@ -63,7 +63,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="minutes_between_blocks", name="Time between Blocks", - unit_of_measurement=TIME_MINUTES, + native_unit_of_measurement=TIME_MINUTES, ), SensorEntityDescription( key="number_of_transactions", @@ -72,7 +72,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="hash_rate", name="Hash rate", - unit_of_measurement=f"PH/{TIME_SECONDS}", + native_unit_of_measurement=f"PH/{TIME_SECONDS}", ), SensorEntityDescription( key="timestamp", @@ -89,22 +89,22 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="total_fees_btc", name="Total fees", - unit_of_measurement="BTC", + native_unit_of_measurement="BTC", ), SensorEntityDescription( key="total_btc_sent", name="Total sent", - unit_of_measurement="BTC", + native_unit_of_measurement="BTC", ), SensorEntityDescription( key="estimated_btc_sent", name="Estimated sent", - unit_of_measurement="BTC", + native_unit_of_measurement="BTC", ), SensorEntityDescription( key="total_btc", name="Total", - unit_of_measurement="BTC", + native_unit_of_measurement="BTC", ), SensorEntityDescription( key="total_blocks", @@ -117,17 +117,17 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="estimated_transaction_volume_usd", name="Est. Transaction volume", - unit_of_measurement="USD", + native_unit_of_measurement="USD", ), SensorEntityDescription( key="miners_revenue_btc", name="Miners revenue", - unit_of_measurement="BTC", + native_unit_of_measurement="BTC", ), SensorEntityDescription( key="market_price_usd", name="Market price", - unit_of_measurement="USD", + native_unit_of_measurement="USD", ), ) @@ -182,48 +182,48 @@ class BitcoinSensor(SensorEntity): sensor_type = self.entity_description.key if sensor_type == "exchangerate": - self._attr_state = ticker[self._currency].p15min - self._attr_unit_of_measurement = self._currency + self._attr_native_value = ticker[self._currency].p15min + self._attr_native_unit_of_measurement = self._currency elif sensor_type == "trade_volume_btc": - self._attr_state = f"{stats.trade_volume_btc:.1f}" + self._attr_native_value = f"{stats.trade_volume_btc:.1f}" elif sensor_type == "miners_revenue_usd": - self._attr_state = f"{stats.miners_revenue_usd:.0f}" + self._attr_native_value = f"{stats.miners_revenue_usd:.0f}" elif sensor_type == "btc_mined": - self._attr_state = str(stats.btc_mined * 0.00000001) + self._attr_native_value = str(stats.btc_mined * 0.00000001) elif sensor_type == "trade_volume_usd": - self._attr_state = f"{stats.trade_volume_usd:.1f}" + self._attr_native_value = f"{stats.trade_volume_usd:.1f}" elif sensor_type == "difficulty": - self._attr_state = f"{stats.difficulty:.0f}" + self._attr_native_value = f"{stats.difficulty:.0f}" elif sensor_type == "minutes_between_blocks": - self._attr_state = f"{stats.minutes_between_blocks:.2f}" + self._attr_native_value = f"{stats.minutes_between_blocks:.2f}" elif sensor_type == "number_of_transactions": - self._attr_state = str(stats.number_of_transactions) + self._attr_native_value = str(stats.number_of_transactions) elif sensor_type == "hash_rate": - self._attr_state = f"{stats.hash_rate * 0.000001:.1f}" + self._attr_native_value = f"{stats.hash_rate * 0.000001:.1f}" elif sensor_type == "timestamp": - self._attr_state = stats.timestamp + self._attr_native_value = stats.timestamp elif sensor_type == "mined_blocks": - self._attr_state = str(stats.mined_blocks) + self._attr_native_value = str(stats.mined_blocks) elif sensor_type == "blocks_size": - self._attr_state = f"{stats.blocks_size:.1f}" + self._attr_native_value = f"{stats.blocks_size:.1f}" elif sensor_type == "total_fees_btc": - self._attr_state = f"{stats.total_fees_btc * 0.00000001:.2f}" + self._attr_native_value = f"{stats.total_fees_btc * 0.00000001:.2f}" elif sensor_type == "total_btc_sent": - self._attr_state = f"{stats.total_btc_sent * 0.00000001:.2f}" + self._attr_native_value = f"{stats.total_btc_sent * 0.00000001:.2f}" elif sensor_type == "estimated_btc_sent": - self._attr_state = f"{stats.estimated_btc_sent * 0.00000001:.2f}" + self._attr_native_value = f"{stats.estimated_btc_sent * 0.00000001:.2f}" elif sensor_type == "total_btc": - self._attr_state = f"{stats.total_btc * 0.00000001:.2f}" + self._attr_native_value = f"{stats.total_btc * 0.00000001:.2f}" elif sensor_type == "total_blocks": - self._attr_state = f"{stats.total_blocks:.0f}" + self._attr_native_value = f"{stats.total_blocks:.0f}" elif sensor_type == "next_retarget": - self._attr_state = f"{stats.next_retarget:.2f}" + self._attr_native_value = f"{stats.next_retarget:.2f}" elif sensor_type == "estimated_transaction_volume_usd": - self._attr_state = f"{stats.estimated_transaction_volume_usd:.2f}" + self._attr_native_value = f"{stats.estimated_transaction_volume_usd:.2f}" elif sensor_type == "miners_revenue_btc": - self._attr_state = f"{stats.miners_revenue_btc * 0.00000001:.1f}" + self._attr_native_value = f"{stats.miners_revenue_btc * 0.00000001:.1f}" elif sensor_type == "market_price_usd": - self._attr_state = f"{stats.market_price_usd:.2f}" + self._attr_native_value = f"{stats.market_price_usd:.2f}" class BitcoinData: diff --git a/homeassistant/components/bizkaibus/sensor.py b/homeassistant/components/bizkaibus/sensor.py index 16f247693af..f83751fb503 100644 --- a/homeassistant/components/bizkaibus/sensor.py +++ b/homeassistant/components/bizkaibus/sensor.py @@ -37,7 +37,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BizkaibusSensor(SensorEntity): """The class for handling the data.""" - _attr_unit_of_measurement = TIME_MINUTES + _attr_native_unit_of_measurement = TIME_MINUTES def __init__(self, data, name): """Initialize the sensor.""" @@ -48,7 +48,7 @@ class BizkaibusSensor(SensorEntity): """Get the latest data from the webservice.""" self.data.update() with suppress(TypeError): - self._attr_state = self.data.info[0][ATTR_DUE_IN] + self._attr_native_value = self.data.info[0][ATTR_DUE_IN] class Bizkaibus: diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py index 09bfca88776..200661dcd1c 100644 --- a/homeassistant/components/blebox/sensor.py +++ b/homeassistant/components/blebox/sensor.py @@ -20,10 +20,10 @@ class BleBoxSensorEntity(BleBoxEntity, SensorEntity): def __init__(self, feature): """Initialize a BleBox sensor feature.""" super().__init__(feature) - self._attr_unit_of_measurement = BLEBOX_TO_UNIT_MAP[feature.unit] + self._attr_native_unit_of_measurement = BLEBOX_TO_UNIT_MAP[feature.unit] self._attr_device_class = BLEBOX_TO_HASS_DEVICE_CLASSES[feature.device_class] @property - def state(self): + def native_value(self): """Return the state.""" return self._feature.current diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py index 1f7cad3f872..88f10183b32 100644 --- a/homeassistant/components/blink/sensor.py +++ b/homeassistant/components/blink/sensor.py @@ -44,7 +44,7 @@ class BlinkSensor(SensorEntity): self._attr_device_class = device_class self.data = data self._camera = data.cameras[camera] - self._attr_unit_of_measurement = units + self._attr_native_unit_of_measurement = units self._attr_unique_id = f"{self._camera.serial}-{sensor_type}" self._sensor_key = ( "temperature_calibrated" if sensor_type == "temperature" else sensor_type @@ -54,9 +54,9 @@ class BlinkSensor(SensorEntity): """Retrieve sensor data from the camera.""" self.data.refresh() try: - self._attr_state = self._camera.attributes[self._sensor_key] + self._attr_native_value = self._camera.attributes[self._sensor_key] except KeyError: - self._attr_state = None + self._attr_native_value = None _LOGGER.error( "%s not a valid camera attribute. Did the API change?", self._sensor_key ) diff --git a/homeassistant/components/blockchain/sensor.py b/homeassistant/components/blockchain/sensor.py index bbb9c892871..9d31d4c0583 100644 --- a/homeassistant/components/blockchain/sensor.py +++ b/homeassistant/components/blockchain/sensor.py @@ -48,7 +48,7 @@ class BlockchainSensor(SensorEntity): _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_icon = ICON - _attr_unit_of_measurement = "BTC" + _attr_native_unit_of_measurement = "BTC" def __init__(self, name, addresses): """Initialize the sensor.""" @@ -57,4 +57,4 @@ class BlockchainSensor(SensorEntity): def update(self): """Get the latest state of the sensor.""" - self._attr_state = get_balance(self.addresses) + self._attr_native_value = get_balance(self.addresses) diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index 7aa2fe9baba..288a1767c7e 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -86,9 +86,13 @@ class BloomSkySensor(SensorEntity): self._sensor_name = sensor_name self._attr_name = f"{device['DeviceName']} {sensor_name}" self._attr_unique_id = f"{self._device_id}-{sensor_name}" - self._attr_unit_of_measurement = SENSOR_UNITS_IMPERIAL.get(sensor_name, None) + self._attr_native_unit_of_measurement = SENSOR_UNITS_IMPERIAL.get( + sensor_name, None + ) if self._bloomsky.is_metric: - self._attr_unit_of_measurement = SENSOR_UNITS_METRIC.get(sensor_name, None) + self._attr_native_unit_of_measurement = SENSOR_UNITS_METRIC.get( + sensor_name, None + ) @property def device_class(self): @@ -99,6 +103,6 @@ class BloomSkySensor(SensorEntity): """Request an update from the BloomSky API.""" self._bloomsky.refresh_devices() state = self._bloomsky.devices[self._device_id]["Data"][self._sensor_name] - self._attr_state = ( + self._attr_native_value = ( f"{state:.2f}" if self._sensor_name in FORMAT_NUMBERS else state ) diff --git a/homeassistant/components/bme280/sensor.py b/homeassistant/components/bme280/sensor.py index 60ce963bf9e..3b9589d0a6a 100644 --- a/homeassistant/components/bme280/sensor.py +++ b/homeassistant/components/bme280/sensor.py @@ -127,11 +127,11 @@ class BME280Sensor(CoordinatorEntity, SensorEntity): self._attr_name = f"{name} {SENSOR_TYPES[sensor_type][0]}" self.temp_unit = temp_unit self.type = sensor_type - self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_native_unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._attr_device_class = SENSOR_TYPES[sensor_type][2] @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.type == SENSOR_TEMP: temperature = round(self.coordinator.data.temperature, 1) diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py index 527a971b237..9669738b2e5 100644 --- a/homeassistant/components/bme680/sensor.py +++ b/homeassistant/components/bme680/sensor.py @@ -327,25 +327,27 @@ class BME680Sensor(SensorEntity): self.bme680_client = bme680_client self.temp_unit = temp_unit self.type = sensor_type - self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_native_unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._attr_device_class = SENSOR_TYPES[sensor_type][2] async def async_update(self): """Get the latest data from the BME680 and update the states.""" await self.hass.async_add_executor_job(self.bme680_client.update) if self.type == SENSOR_TEMP: - self._attr_state = round(self.bme680_client.sensor_data.temperature, 1) + self._attr_native_value = round( + self.bme680_client.sensor_data.temperature, 1 + ) if self.temp_unit == TEMP_FAHRENHEIT: - self._attr_state = round(celsius_to_fahrenheit(self.state), 1) + self._attr_native_value = round(celsius_to_fahrenheit(self.state), 1) elif self.type == SENSOR_HUMID: - self._attr_state = round(self.bme680_client.sensor_data.humidity, 1) + self._attr_native_value = round(self.bme680_client.sensor_data.humidity, 1) elif self.type == SENSOR_PRESS: - self._attr_state = round(self.bme680_client.sensor_data.pressure, 1) + self._attr_native_value = round(self.bme680_client.sensor_data.pressure, 1) elif self.type == SENSOR_GAS: - self._attr_state = int( + self._attr_native_value = int( round(self.bme680_client.sensor_data.gas_resistance, 0) ) elif self.type == SENSOR_AQ: aq_score = self.bme680_client.sensor_data.air_quality if aq_score is not None: - self._attr_state = round(aq_score, 1) + self._attr_native_value = round(aq_score, 1) diff --git a/homeassistant/components/bmp280/sensor.py b/homeassistant/components/bmp280/sensor.py index 7bf355bb736..21ab71e5ce6 100644 --- a/homeassistant/components/bmp280/sensor.py +++ b/homeassistant/components/bmp280/sensor.py @@ -78,7 +78,7 @@ class Bmp280Sensor(SensorEntity): """Initialize the sensor.""" self._bmp280 = bmp280 self._attr_name = name - self._attr_unit_of_measurement = unit_of_measurement + self._attr_native_unit_of_measurement = unit_of_measurement class Bmp280TemperatureSensor(Bmp280Sensor): @@ -94,7 +94,7 @@ class Bmp280TemperatureSensor(Bmp280Sensor): def update(self): """Fetch new state data for the sensor.""" try: - self._attr_state = round(self._bmp280.temperature, 1) + self._attr_native_value = round(self._bmp280.temperature, 1) if not self.available: _LOGGER.warning("Communication restored with temperature sensor") self._attr_available = True @@ -119,7 +119,7 @@ class Bmp280PressureSensor(Bmp280Sensor): def update(self): """Fetch new state data for the sensor.""" try: - self._attr_state = round(self._bmp280.pressure) + self._attr_native_value = round(self._bmp280.pressure) if not self.available: _LOGGER.warning("Communication restored with pressure sensor") self._attr_available = True diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index df899496339..76d183bf8e8 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -516,7 +516,7 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): self._attr_device_class = attribute_info.get( attribute, [None, None, None, None] )[1] - self._attr_unit_of_measurement = attribute_info.get( + self._attr_native_unit_of_measurement = attribute_info.get( attribute, [None, None, None, None] )[2] @@ -525,24 +525,24 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): _LOGGER.debug("Updating %s", self._vehicle.name) vehicle_state = self._vehicle.state if self._attribute == "charging_status": - self._attr_state = getattr(vehicle_state, self._attribute).value + self._attr_native_value = getattr(vehicle_state, self._attribute).value elif self.unit_of_measurement == VOLUME_GALLONS: value = getattr(vehicle_state, self._attribute) value_converted = self.hass.config.units.volume(value, VOLUME_LITERS) - self._attr_state = round(value_converted) + self._attr_native_value = round(value_converted) elif self.unit_of_measurement == LENGTH_MILES: value = getattr(vehicle_state, self._attribute) value_converted = self.hass.config.units.length(value, LENGTH_KILOMETERS) - self._attr_state = round(value_converted) + self._attr_native_value = round(value_converted) elif self._service is None: - self._attr_state = getattr(vehicle_state, self._attribute) + self._attr_native_value = getattr(vehicle_state, self._attribute) elif self._service == SERVICE_LAST_TRIP: vehicle_last_trip = self._vehicle.state.last_trip if self._attribute == "date_utc": date_str = getattr(vehicle_last_trip, "date") - self._attr_state = dt_util.parse_datetime(date_str).isoformat() + self._attr_native_value = dt_util.parse_datetime(date_str).isoformat() else: - self._attr_state = getattr(vehicle_last_trip, self._attribute) + self._attr_native_value = getattr(vehicle_last_trip, self._attribute) elif self._service == SERVICE_ALL_TRIPS: vehicle_all_trips = self._vehicle.state.all_trips for attribute in ( @@ -555,13 +555,13 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): if self._attribute.startswith(f"{attribute}_"): attr = getattr(vehicle_all_trips, attribute) sub_attr = self._attribute.replace(f"{attribute}_", "") - self._attr_state = getattr(attr, sub_attr) + self._attr_native_value = getattr(attr, sub_attr) return if self._attribute == "reset_date_utc": date_str = getattr(vehicle_all_trips, "reset_date") - self._attr_state = dt_util.parse_datetime(date_str).isoformat() + self._attr_native_value = dt_util.parse_datetime(date_str).isoformat() else: - self._attr_state = getattr(vehicle_all_trips, self._attribute) + self._attr_native_value = getattr(vehicle_all_trips, self._attribute) vehicle_state = self._vehicle.state charging_state = vehicle_state.charging_status in [ChargingState.CHARGING] diff --git a/homeassistant/components/bosch_shc/sensor.py b/homeassistant/components/bosch_shc/sensor.py index 55aa1eb5772..6ea4f3c7065 100644 --- a/homeassistant/components/bosch_shc/sensor.py +++ b/homeassistant/components/bosch_shc/sensor.py @@ -147,7 +147,7 @@ class TemperatureSensor(SHCEntity, SensorEntity): """Representation of an SHC temperature reporting sensor.""" _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_unit_of_measurement = TEMP_CELSIUS + _attr_native_unit_of_measurement = TEMP_CELSIUS def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC temperature reporting sensor.""" @@ -156,7 +156,7 @@ class TemperatureSensor(SHCEntity, SensorEntity): self._attr_unique_id = f"{device.serial}_temperature" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.temperature @@ -165,7 +165,7 @@ class HumiditySensor(SHCEntity, SensorEntity): """Representation of an SHC humidity reporting sensor.""" _attr_device_class = DEVICE_CLASS_HUMIDITY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC humidity reporting sensor.""" @@ -174,7 +174,7 @@ class HumiditySensor(SHCEntity, SensorEntity): self._attr_unique_id = f"{device.serial}_humidity" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.humidity @@ -183,7 +183,7 @@ class PuritySensor(SHCEntity, SensorEntity): """Representation of an SHC purity reporting sensor.""" _attr_icon = "mdi:molecule-co2" - _attr_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION + _attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC purity reporting sensor.""" @@ -192,7 +192,7 @@ class PuritySensor(SHCEntity, SensorEntity): self._attr_unique_id = f"{device.serial}_purity" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.purity @@ -207,7 +207,7 @@ class AirQualitySensor(SHCEntity, SensorEntity): self._attr_unique_id = f"{device.serial}_airquality" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.combined_rating.name @@ -229,7 +229,7 @@ class TemperatureRatingSensor(SHCEntity, SensorEntity): self._attr_unique_id = f"{device.serial}_temperature_rating" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.temperature_rating.name @@ -244,7 +244,7 @@ class HumidityRatingSensor(SHCEntity, SensorEntity): self._attr_unique_id = f"{device.serial}_humidity_rating" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.humidity_rating.name @@ -259,7 +259,7 @@ class PurityRatingSensor(SHCEntity, SensorEntity): self._attr_unique_id = f"{device.serial}_purity_rating" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.purity_rating.name @@ -268,7 +268,7 @@ class PowerSensor(SHCEntity, SensorEntity): """Representation of an SHC power reporting sensor.""" _attr_device_class = DEVICE_CLASS_POWER - _attr_unit_of_measurement = POWER_WATT + _attr_native_unit_of_measurement = POWER_WATT def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC power reporting sensor.""" @@ -277,7 +277,7 @@ class PowerSensor(SHCEntity, SensorEntity): self._attr_unique_id = f"{device.serial}_power" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.powerconsumption @@ -286,7 +286,7 @@ class EnergySensor(SHCEntity, SensorEntity): """Representation of an SHC energy reporting sensor.""" _attr_device_class = DEVICE_CLASS_ENERGY - _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR + _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC energy reporting sensor.""" @@ -295,7 +295,7 @@ class EnergySensor(SHCEntity, SensorEntity): self._attr_unique_id = f"{self._device.serial}_energy" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.energyconsumption / 1000.0 @@ -304,7 +304,7 @@ class ValveTappetSensor(SHCEntity, SensorEntity): """Representation of an SHC valve tappet reporting sensor.""" _attr_icon = "mdi:gauge" - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC valve tappet reporting sensor.""" @@ -313,7 +313,7 @@ class ValveTappetSensor(SHCEntity, SensorEntity): self._attr_unique_id = f"{device.serial}_valvetappet" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.position diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 30bc8047d03..f708790a5ce 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -91,10 +91,10 @@ class BroadlinkSensor(BroadlinkEntity, SensorEntity): self._attr_device_class = SENSOR_TYPES[monitored_condition][2] self._attr_name = f"{device.name} {SENSOR_TYPES[monitored_condition][0]}" self._attr_state_class = SENSOR_TYPES[monitored_condition][3] - self._attr_state = self._coordinator.data[monitored_condition] + self._attr_native_value = self._coordinator.data[monitored_condition] self._attr_unique_id = f"{device.unique_id}-{monitored_condition}" - self._attr_unit_of_measurement = SENSOR_TYPES[monitored_condition][1] + self._attr_native_unit_of_measurement = SENSOR_TYPES[monitored_condition][1] def _update_state(self, data): """Update the state of the entity.""" - self._attr_state = data[self._monitored_condition] + self._attr_native_value = data[self._monitored_condition] diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index 95ffcf063f2..8e34f9f983b 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -87,154 +87,154 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( key=ATTR_PAGE_COUNTER, icon="mdi:file-document-outline", name=ATTR_PAGE_COUNTER.replace("_", " ").title(), - unit_of_measurement=UNIT_PAGES, + native_unit_of_measurement=UNIT_PAGES, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_BW_COUNTER, icon="mdi:file-document-outline", name=ATTR_BW_COUNTER.replace("_", " ").title(), - unit_of_measurement=UNIT_PAGES, + native_unit_of_measurement=UNIT_PAGES, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_COLOR_COUNTER, icon="mdi:file-document-outline", name=ATTR_COLOR_COUNTER.replace("_", " ").title(), - unit_of_measurement=UNIT_PAGES, + native_unit_of_measurement=UNIT_PAGES, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_DUPLEX_COUNTER, icon="mdi:file-document-outline", name=ATTR_DUPLEX_COUNTER.replace("_", " ").title(), - unit_of_measurement=UNIT_PAGES, + native_unit_of_measurement=UNIT_PAGES, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", name=ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_BLACK_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", name=ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_CYAN_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", name=ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_MAGENTA_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", name=ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_YELLOW_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", name=ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_BELT_UNIT_REMAINING_LIFE, icon="mdi:current-ac", name=ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_FUSER_REMAINING_LIFE, icon="mdi:water-outline", name=ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_LASER_REMAINING_LIFE, icon="mdi:spotlight-beam", name=ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_PF_KIT_1_REMAINING_LIFE, icon="mdi:printer-3d", name=ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_PF_KIT_MP_REMAINING_LIFE, icon="mdi:printer-3d", name=ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_BLACK_TONER_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_CYAN_TONER_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_MAGENTA_TONER_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_YELLOW_TONER_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_BLACK_INK_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_CYAN_INK_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_MAGENTA_INK_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_YELLOW_INK_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 0ff5c14d9cc..8dd150b48bf 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -64,7 +64,7 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): self.entity_description = description @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state.""" if self.entity_description.key == ATTR_UPTIME: return cast( diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index c327f9122ce..22d9ea8e5d8 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -103,4 +103,4 @@ class BrottsplatskartanSensor(SensorEntity): ATTR_ATTRIBUTION: brottsplatskartan.ATTRIBUTION } self._attr_extra_state_attributes.update(incident_counts) - self._attr_state = len(incidents) + self._attr_native_value = len(incidents) diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 7af84f48af7..1d349fe6f53 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -364,7 +364,7 @@ class BrSensor(SensorEntity): self._attr_name = f"{client_name} {SENSOR_TYPES[sensor_type][0]}" self._attr_icon = SENSOR_TYPES[sensor_type][2] self.type = sensor_type - self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_native_unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._measured = None self._attr_unique_id = "{:2.6f}{:2.6f}{}".format( coordinates[CONF_LATITUDE], coordinates[CONF_LONGITUDE], sensor_type @@ -438,7 +438,7 @@ class BrSensor(SensorEntity): img = condition.get(IMAGE) if new_state != self.state or img != self.entity_picture: - self._attr_state = new_state + self._attr_native_value = new_state self._attr_entity_picture = img return True return False @@ -446,9 +446,11 @@ class BrSensor(SensorEntity): if self.type.startswith(WINDSPEED): # hass wants windspeeds in km/h not m/s, so convert: try: - self._attr_state = data.get(FORECAST)[fcday].get(self.type[:-3]) + self._attr_native_value = data.get(FORECAST)[fcday].get( + self.type[:-3] + ) if self.state is not None: - self._attr_state = round(self.state * 3.6, 1) + self._attr_native_value = round(self.state * 3.6, 1) return True except IndexError: _LOGGER.warning("No forecast for fcday=%s", fcday) @@ -456,7 +458,7 @@ class BrSensor(SensorEntity): # update all other sensors try: - self._attr_state = data.get(FORECAST)[fcday].get(self.type[:-3]) + self._attr_native_value = data.get(FORECAST)[fcday].get(self.type[:-3]) return True except IndexError: _LOGGER.warning("No forecast for fcday=%s", fcday) @@ -480,7 +482,7 @@ class BrSensor(SensorEntity): img = condition.get(IMAGE) if new_state != self.state or img != self.entity_picture: - self._attr_state = new_state + self._attr_native_value = new_state self._attr_entity_picture = img return True @@ -490,25 +492,27 @@ class BrSensor(SensorEntity): # update nested precipitation forecast sensors nested = data.get(PRECIPITATION_FORECAST) self._timeframe = nested.get(TIMEFRAME) - self._attr_state = nested.get(self.type[len(PRECIPITATION_FORECAST) + 1 :]) + self._attr_native_value = nested.get( + self.type[len(PRECIPITATION_FORECAST) + 1 :] + ) return True if self.type in [WINDSPEED, WINDGUST]: # hass wants windspeeds in km/h not m/s, so convert: - self._attr_state = data.get(self.type) + self._attr_native_value = data.get(self.type) if self.state is not None: - self._attr_state = round(data.get(self.type) * 3.6, 1) + self._attr_native_value = round(data.get(self.type) * 3.6, 1) return True if self.type == VISIBILITY: # hass wants visibility in km (not m), so convert: - self._attr_state = data.get(self.type) + self._attr_native_value = data.get(self.type) if self.state is not None: - self._attr_state = round(self.state / 1000, 1) + self._attr_native_value = round(self.state / 1000, 1) return True # update all other sensors - self._attr_state = data.get(self.type) + self._attr_native_value = data.get(self.type) if self.type.startswith(PRECIPITATION_FORECAST): result = {ATTR_ATTRIBUTION: data.get(ATTRIBUTION)} if self._timeframe is not None: diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index acb885055a3..3870cb357ef 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -144,7 +144,7 @@ class CanarySensor(CoordinatorEntity, SensorEntity): return None @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the state of the sensor.""" return self.reading diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 787465bb6f3..7b6445a2f35 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -85,7 +85,7 @@ class SSLCertificateTimestamp(CertExpiryEntity, SensorEntity): self._attr_unique_id = f"{coordinator.host}:{coordinator.port}-timestamp" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.coordinator.data: return self.coordinator.data.isoformat() diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index 7d54d259051..fd0c96c6fbe 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -265,7 +265,7 @@ class CityBikesNetwork: class CityBikesStation(SensorEntity): """CityBikes API Sensor.""" - _attr_unit_of_measurement = "bikes" + _attr_native_unit_of_measurement = "bikes" _attr_icon = "mdi:bike" def __init__(self, network, station_id, entity_id): @@ -281,7 +281,7 @@ class CityBikesStation(SensorEntity): station_data = station break self._attr_name = station_data.get(ATTR_NAME) - self._attr_state = station_data.get(ATTR_FREE_BIKES) + self._attr_native_value = station_data.get(ATTR_FREE_BIKES) self._attr_extra_state_attributes = ( { ATTR_ATTRIBUTION: CITYBIKES_ATTRIBUTION, diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py index 3f96dd9e02c..1ba5bbe3a34 100644 --- a/homeassistant/components/climacell/sensor.py +++ b/homeassistant/components/climacell/sensor.py @@ -68,7 +68,7 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): f"{self._config_entry.unique_id}_{slugify(description.name)}" ) self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: self.attribution} - self._attr_unit_of_measurement = ( + self._attr_native_unit_of_measurement = ( description.unit_metric if hass.config.units.is_metric else description.unit_imperial @@ -80,7 +80,7 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): """Return the raw state.""" @property - def state(self) -> str | int | float | None: + def native_value(self) -> str | int | float | None: """Return the state.""" state = self._state if ( diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index ea1cd1f6169..a4c1062e2c6 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -123,12 +123,12 @@ class CO2Sensor(update_coordinator.CoordinatorEntity[CO2SignalResponse], SensorE ) @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return sensor state.""" return round(self.coordinator.data["data"][self._description.key], 2) # type: ignore[misc] @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement.""" if self._description.unit_of_measurement: return self._description.unit_of_measurement diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index f836a604f6a..d5abb7d66f5 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -116,12 +116,12 @@ class AccountSensor(SensorEntity): return self._id @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return self._unit_of_measurement @@ -181,12 +181,12 @@ class ExchangeRateSensor(SensorEntity): return self._id @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return self._unit_of_measurement diff --git a/homeassistant/components/comed_hourly_pricing/sensor.py b/homeassistant/components/comed_hourly_pricing/sensor.py index 5d4ec6eec13..48ec0c46536 100644 --- a/homeassistant/components/comed_hourly_pricing/sensor.py +++ b/homeassistant/components/comed_hourly_pricing/sensor.py @@ -86,12 +86,12 @@ class ComedHourlyPricingSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index 728bc13b76b..a6a625bab99 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -297,7 +297,7 @@ class ComfoConnectSensor(SensorEntity): self.schedule_update_ha_state() @property - def state(self): + def native_value(self): """Return the state of the entity.""" try: return self._ccb.data[self._sensor_id] @@ -325,7 +325,7 @@ class ComfoConnectSensor(SensorEntity): return SENSOR_TYPES[self._sensor_type][ATTR_ICON] @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity.""" return SENSOR_TYPES[self._sensor_type][ATTR_UNIT] diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index 10c5a16f60b..43e05a429b6 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -84,12 +84,12 @@ class CommandSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index 35ca07ce522..257c6b4a354 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -107,7 +107,7 @@ class CompensationSensor(SensorEntity): return False @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -123,7 +123,7 @@ class CompensationSensor(SensorEntity): return ret @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/coronavirus/sensor.py b/homeassistant/components/coronavirus/sensor.py index b467a5fee12..92fdf232214 100644 --- a/homeassistant/components/coronavirus/sensor.py +++ b/homeassistant/components/coronavirus/sensor.py @@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class CoronavirusSensor(CoordinatorEntity, SensorEntity): """Sensor representing corona virus data.""" - _attr_unit_of_measurement = "people" + _attr_native_unit_of_measurement = "people" def __init__(self, coordinator, country, info_type): """Initialize coronavirus sensor.""" @@ -53,7 +53,7 @@ class CoronavirusSensor(CoordinatorEntity, SensorEntity): ) @property - def state(self): + def native_value(self): """State of the sensor.""" if self.country == OPTION_WORLDWIDE: sum_cases = 0 diff --git a/homeassistant/components/cpuspeed/sensor.py b/homeassistant/components/cpuspeed/sensor.py index 01938344694..c34ea939de7 100644 --- a/homeassistant/components/cpuspeed/sensor.py +++ b/homeassistant/components/cpuspeed/sensor.py @@ -43,12 +43,12 @@ class CpuSpeedSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return FREQUENCY_GIGAHERTZ diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index 6a3fc7b4215..74d3d9a36a2 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -111,7 +111,7 @@ class CupsSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._printer is None: return None @@ -183,7 +183,7 @@ class IPPSensor(SensorEntity): return self._available @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._attributes is None: return None @@ -257,7 +257,7 @@ class MarkerSensor(SensorEntity): return ICON_MARKER @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._attributes is None: return None @@ -265,7 +265,7 @@ class MarkerSensor(SensorEntity): return self._attributes[self._printer]["marker-levels"][self._index] @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return PERCENTAGE diff --git a/homeassistant/components/currencylayer/sensor.py b/homeassistant/components/currencylayer/sensor.py index f42534f509b..fd3f3b2f8c5 100644 --- a/homeassistant/components/currencylayer/sensor.py +++ b/homeassistant/components/currencylayer/sensor.py @@ -65,7 +65,7 @@ class CurrencylayerSensor(SensorEntity): self._state = None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._quote @@ -80,7 +80,7 @@ class CurrencylayerSensor(SensorEntity): return ICON @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index e36640b1c1d..c88b7da13f4 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Mapping +from contextlib import suppress from dataclasses import dataclass from datetime import datetime, timedelta import logging @@ -26,6 +27,8 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLTAGE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ( # noqa: F401 @@ -34,7 +37,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 +from homeassistant.helpers.typing import ConfigType, StateType _LOGGER: Final = logging.getLogger(__name__) @@ -102,14 +105,18 @@ class SensorEntityDescription(EntityDescription): state_class: str | None = None last_reset: datetime | None = None + native_unit_of_measurement: str | None = None class SensorEntity(Entity): """Base class for sensor entities.""" entity_description: SensorEntityDescription - _attr_state_class: str | None _attr_last_reset: datetime | None + _attr_native_unit_of_measurement: str | None + _attr_native_value: StateType = None + _attr_state_class: str | None + _temperature_conversion_reported = False @property def state_class(self) -> str | None: @@ -145,3 +152,94 @@ class SensorEntity(Entity): return {ATTR_LAST_RESET: last_reset.isoformat()} return None + + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" + return self._attr_native_value + + @property + def native_unit_of_measurement(self) -> str | None: + """Return the unit of measurement of the sensor, if any.""" + if hasattr(self, "_attr_native_unit_of_measurement"): + return self._attr_native_unit_of_measurement + if hasattr(self, "entity_description"): + return self.entity_description.native_unit_of_measurement + return None + + @property + def unit_of_measurement(self) -> str | None: + """Return the unit of measurement of the entity, after unit conversion.""" + if ( + hasattr(self, "_attr_unit_of_measurement") + and self._attr_unit_of_measurement is not None + ): + return self._attr_unit_of_measurement + if ( + hasattr(self, "entity_description") + and self.entity_description.unit_of_measurement is not None + ): + return self.entity_description.unit_of_measurement + + native_unit_of_measurement = self.native_unit_of_measurement + + if native_unit_of_measurement in (TEMP_CELSIUS, TEMP_FAHRENHEIT): + return self.hass.config.units.temperature_unit + + return native_unit_of_measurement + + @property + def state(self) -> Any: + """Return the state of the sensor and perform unit conversions, if needed.""" + # Test if _attr_state has been set in this instance + if "_attr_state" in self.__dict__: + return self._attr_state + + unit_of_measurement = self.native_unit_of_measurement + value = self.native_value + + units = self.hass.config.units + if ( + value is not None + and unit_of_measurement in (TEMP_CELSIUS, TEMP_FAHRENHEIT) + and unit_of_measurement != units.temperature_unit + ): + if ( + self.device_class != DEVICE_CLASS_TEMPERATURE + and not self._temperature_conversion_reported + ): + self._temperature_conversion_reported = True + report_issue = self._suggest_report_issue() + _LOGGER.warning( + "Entity %s (%s) with device_class %s reports a temperature in " + "%s which will be converted to %s. Temperature conversion for " + "entities without correct device_class is deprecated and will" + " be removed from Home Assistant Core 2022.3. Please update " + "your configuration if device_class is manually configured, " + "otherwise %s", + self.entity_id, + type(self), + self.device_class, + unit_of_measurement, + units.temperature_unit, + report_issue, + ) + value_s = str(value) + prec = len(value_s) - value_s.index(".") - 1 if "." in value_s else 0 + # Suppress ValueError (Could not convert sensor_value to float) + with suppress(ValueError): + temp = units.temperature(float(value), unit_of_measurement) + value = str(round(temp) if prec == 0 else round(temp, prec)) + + return value + + def __repr__(self) -> str: + """Return the representation. + + Entity.__repr__ includes the state in the generated string, this fails if we're + called before self.hass is set. + """ + if not self.hass: + return f"" + + return super().__repr__() diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 131460baa93..63e84371fad 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -539,25 +539,13 @@ class Entity(ABC): if end - start > 0.4 and not self._slow_reported: self._slow_reported = True - extra = "" - if "custom_components" in type(self).__module__: - extra = "Please report it to the custom component author." - else: - extra = ( - "Please create a bug report at " - "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" - ) - if self.platform: - extra += ( - f"+label%3A%22integration%3A+{self.platform.platform_name}%22" - ) - + report_issue = self._suggest_report_issue() _LOGGER.warning( - "Updating state for %s (%s) took %.3f seconds. %s", + "Updating state for %s (%s) took %.3f seconds. Please %s", self.entity_id, type(self), end - start, - extra, + report_issue, ) # Overwrite properties that have been set in the config file. @@ -858,6 +846,23 @@ class Entity(ABC): if self.parallel_updates: self.parallel_updates.release() + def _suggest_report_issue(self) -> str: + """Suggest to report an issue.""" + report_issue = "" + if "custom_components" in type(self).__module__: + report_issue = "report it to the custom component author." + else: + report_issue = ( + "create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" + ) + if self.platform: + report_issue += ( + f"+label%3A%22integration%3A+{self.platform.platform_name}%22" + ) + + return report_issue + @dataclass class ToggleEntityDescription(EntityDescription): diff --git a/tests/components/arlo/test_sensor.py b/tests/components/arlo/test_sensor.py index bf67ab21c97..2c1f3e26b54 100644 --- a/tests/components/arlo/test_sensor.py +++ b/tests/components/arlo/test_sensor.py @@ -19,53 +19,55 @@ def _get_named_tuple(input_dict): return namedtuple("Struct", input_dict.keys())(*input_dict.values()) -def _get_sensor(name="Last", sensor_type="last_capture", data=None): +def _get_sensor(hass, name="Last", sensor_type="last_capture", data=None): if data is None: data = {} sensor_entry = next( sensor_entry for sensor_entry in SENSOR_TYPES if sensor_entry.key == sensor_type ) sensor_entry.name = name - return arlo.ArloSensor(data, sensor_entry) + sensor = arlo.ArloSensor(data, sensor_entry) + sensor.hass = hass + return sensor @pytest.fixture() -def default_sensor(): +def default_sensor(hass): """Create an ArloSensor with default values.""" - return _get_sensor() + return _get_sensor(hass) @pytest.fixture() -def battery_sensor(): +def battery_sensor(hass): """Create an ArloSensor with battery data.""" data = _get_named_tuple({"battery_level": 50}) - return _get_sensor("Battery Level", "battery_level", data) + return _get_sensor(hass, "Battery Level", "battery_level", data) @pytest.fixture() -def temperature_sensor(): +def temperature_sensor(hass): """Create a temperature ArloSensor.""" - return _get_sensor("Temperature", "temperature") + return _get_sensor(hass, "Temperature", "temperature") @pytest.fixture() -def humidity_sensor(): +def humidity_sensor(hass): """Create a humidity ArloSensor.""" - return _get_sensor("Humidity", "humidity") + return _get_sensor(hass, "Humidity", "humidity") @pytest.fixture() -def cameras_sensor(): +def cameras_sensor(hass): """Create a total cameras ArloSensor.""" data = _get_named_tuple({"cameras": [0, 0]}) - return _get_sensor("Arlo Cameras", "total_cameras", data) + return _get_sensor(hass, "Arlo Cameras", "total_cameras", data) @pytest.fixture() -def captured_sensor(): +def captured_sensor(hass): """Create a captured today ArloSensor.""" data = _get_named_tuple({"captured_today": [0, 0, 0, 0, 0]}) - return _get_sensor("Captured Today", "captured_today", data) + return _get_sensor(hass, "Captured Today", "captured_today", data) class PlatformSetupFixture: @@ -88,14 +90,6 @@ def platform_setup(): return PlatformSetupFixture() -@pytest.fixture() -def sensor_with_hass_data(default_sensor, hass): - """Create a sensor with async_dispatcher_connected mocked.""" - hass.data = {} - default_sensor.hass = hass - return default_sensor - - @pytest.fixture() def mock_dispatch(): """Mock the dispatcher connect method.""" @@ -145,14 +139,14 @@ def test_sensor_name(default_sensor): assert default_sensor.name == "Last" -async def test_async_added_to_hass(sensor_with_hass_data, mock_dispatch): +async def test_async_added_to_hass(default_sensor, mock_dispatch): """Test dispatcher called when added.""" - await sensor_with_hass_data.async_added_to_hass() + await default_sensor.async_added_to_hass() assert len(mock_dispatch.mock_calls) == 1 kall = mock_dispatch.call_args args, kwargs = kall assert len(args) == 3 - assert args[0] == sensor_with_hass_data.hass + assert args[0] == default_sensor.hass assert args[1] == "arlo_update" assert not kwargs @@ -197,22 +191,22 @@ def test_update_captured_today(captured_sensor): assert captured_sensor.state == 5 -def _test_attributes(sensor_type): +def _test_attributes(hass, sensor_type): data = _get_named_tuple({"model_id": "TEST123"}) - sensor = _get_sensor("test", sensor_type, data) + sensor = _get_sensor(hass, "test", sensor_type, data) attrs = sensor.extra_state_attributes assert attrs.get(ATTR_ATTRIBUTION) == "Data provided by arlo.netgear.com" assert attrs.get("brand") == "Netgear Arlo" assert attrs.get("model") == "TEST123" -def test_state_attributes(): +def test_state_attributes(hass): """Test attributes for camera sensor types.""" - _test_attributes("battery_level") - _test_attributes("signal_strength") - _test_attributes("temperature") - _test_attributes("humidity") - _test_attributes("air_quality") + _test_attributes(hass, "battery_level") + _test_attributes(hass, "signal_strength") + _test_attributes(hass, "temperature") + _test_attributes(hass, "humidity") + _test_attributes(hass, "air_quality") def test_attributes_total_cameras(cameras_sensor): @@ -223,17 +217,17 @@ def test_attributes_total_cameras(cameras_sensor): assert attrs.get("model") is None -def _test_update(sensor_type, key, value): +def _test_update(hass, sensor_type, key, value): data = _get_named_tuple({key: value}) - sensor = _get_sensor("test", sensor_type, data) + sensor = _get_sensor(hass, "test", sensor_type, data) sensor.update() assert sensor.state == value -def test_update(): +def test_update(hass): """Test update method for direct transcription sensor types.""" - _test_update("battery_level", "battery_level", 100) - _test_update("signal_strength", "signal_strength", 100) - _test_update("temperature", "ambient_temperature", 21.4) - _test_update("humidity", "ambient_humidity", 45.1) - _test_update("air_quality", "ambient_air_quality", 14.2) + _test_update(hass, "battery_level", "battery_level", 100) + _test_update(hass, "signal_strength", "signal_strength", 100) + _test_update(hass, "temperature", "ambient_temperature", 21.4) + _test_update(hass, "humidity", "ambient_humidity", 45.1) + _test_update(hass, "air_quality", "ambient_air_quality", 14.2) diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py new file mode 100644 index 00000000000..f09cd489489 --- /dev/null +++ b/tests/components/sensor/test_init.py @@ -0,0 +1,30 @@ +"""The test for sensor device automation.""" +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.setup import async_setup_component + + +async def test_deprecated_temperature_conversion( + hass, caplog, enable_custom_integrations +): + """Test warning on deprecated temperature conversion.""" + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", native_value="0.0", native_unit_of_measurement=TEMP_FAHRENHEIT + ) + + entity0 = platform.ENTITIES["0"] + assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.state == "-17.8" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert ( + "Entity sensor.test () " + "with device_class None reports a temperature in °F which will be converted to " + "°C. Temperature conversion for entities without correct device_class is " + "deprecated and will be removed from Home Assistant Core 2022.3. Please update " + "your configuration if device_class is manually configured, otherwise report it " + "to the custom component author." + ) in caplog.text diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index 384db20d2d4..7c121d1c05a 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -61,7 +61,7 @@ async def async_setup_platform( async_add_entities_callback(list(ENTITIES.values())) -class MockSensor(MockEntity): +class MockSensor(MockEntity, sensor.SensorEntity): """Mock Sensor class.""" @property @@ -70,6 +70,11 @@ class MockSensor(MockEntity): return self._handle("device_class") @property - def unit_of_measurement(self): - """Return the unit_of_measurement of this sensor.""" - return self._handle("unit_of_measurement") + def native_unit_of_measurement(self): + """Return the native unit_of_measurement of this sensor.""" + return self._handle("native_unit_of_measurement") + + @property + def native_value(self): + """Return the native value of this sensor.""" + return self._handle("native_value") From 2d669a4613d41a00b75592edaca999db52bfe981 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 11 Aug 2021 11:07:04 +0200 Subject: [PATCH 146/355] Remove legacy code. (#54452) --- homeassistant/components/tradfri/__init__.py | 21 +----- homeassistant/components/tradfri/const.py | 1 - tests/components/tradfri/test_init.py | 69 -------------------- 3 files changed, 1 insertion(+), 90 deletions(-) diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 8508dab5b96..e2c90098314 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -17,7 +17,6 @@ from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType -from homeassistant.util.json import load_json from .const import ( ATTR_TRADFRI_GATEWAY, @@ -29,7 +28,6 @@ from .const import ( CONF_IDENTITY, CONF_IMPORT_GROUPS, CONF_KEY, - CONFIG_FILE, DEFAULT_ALLOW_TRADFRI_GROUPS, DEVICES, DOMAIN, @@ -69,27 +67,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType): entry.data.get("host") for entry in hass.config_entries.async_entries(DOMAIN) ] - legacy_hosts = await hass.async_add_executor_job( - load_json, hass.config.path(CONFIG_FILE) - ) - - for host, info in legacy_hosts.items(): # type: ignore - if host in configured_hosts: - continue - - info[CONF_HOST] = host - info[CONF_IMPORT_GROUPS] = conf[CONF_ALLOW_TRADFRI_GROUPS] - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=info - ) - ) - host = conf.get(CONF_HOST) import_groups = conf[CONF_ALLOW_TRADFRI_GROUPS] - if host is None or host in configured_hosts or host in legacy_hosts: + if host is None or host in configured_hosts: return True hass.async_create_task( diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py index f7c2bf6cbe5..1f382548263 100644 --- a/homeassistant/components/tradfri/const.py +++ b/homeassistant/components/tradfri/const.py @@ -15,7 +15,6 @@ CONF_IDENTITY = "identity" CONF_IMPORT_GROUPS = "import_groups" CONF_GATEWAY_ID = "gateway_id" CONF_KEY = "key" -CONFIG_FILE = ".tradfri_psk.conf" DEFAULT_ALLOW_TRADFRI_GROUPS = False DOMAIN = "tradfri" KEY_API = "tradfri_api" diff --git a/tests/components/tradfri/test_init.py b/tests/components/tradfri/test_init.py index 8e11ab06f34..e8cc83a456c 100644 --- a/tests/components/tradfri/test_init.py +++ b/tests/components/tradfri/test_init.py @@ -1,81 +1,12 @@ """Tests for Tradfri setup.""" from unittest.mock import patch -from homeassistant import config_entries from homeassistant.components import tradfri from homeassistant.helpers import device_registry as dr -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -async def test_config_yaml_host_not_imported(hass): - """Test that we don't import a configured host.""" - MockConfigEntry(domain="tradfri", data={"host": "mock-host"}).add_to_hass(hass) - - with patch( - "homeassistant.components.tradfri.load_json", return_value={} - ), patch.object(hass.config_entries.flow, "async_init") as mock_init: - assert await async_setup_component( - hass, "tradfri", {"tradfri": {"host": "mock-host"}} - ) - await hass.async_block_till_done() - - assert len(mock_init.mock_calls) == 0 - - -async def test_config_yaml_host_imported(hass): - """Test that we import a configured host.""" - with patch("homeassistant.components.tradfri.load_json", return_value={}): - assert await async_setup_component( - hass, "tradfri", {"tradfri": {"host": "mock-host"}} - ) - await hass.async_block_till_done() - - progress = hass.config_entries.flow.async_progress() - assert len(progress) == 1 - assert progress[0]["handler"] == "tradfri" - assert progress[0]["context"] == {"source": config_entries.SOURCE_IMPORT} - - -async def test_config_json_host_not_imported(hass): - """Test that we don't import a configured host.""" - MockConfigEntry(domain="tradfri", data={"host": "mock-host"}).add_to_hass(hass) - - with patch( - "homeassistant.components.tradfri.load_json", - return_value={"mock-host": {"key": "some-info"}}, - ), patch.object(hass.config_entries.flow, "async_init") as mock_init: - assert await async_setup_component(hass, "tradfri", {"tradfri": {}}) - await hass.async_block_till_done() - - assert len(mock_init.mock_calls) == 0 - - -async def test_config_json_host_imported( - hass, mock_gateway_info, mock_entry_setup, gateway_id -): - """Test that we import a configured host.""" - mock_gateway_info.side_effect = lambda hass, host, identity, key: { - "host": host, - "identity": identity, - "key": key, - "gateway_id": gateway_id, - } - - with patch( - "homeassistant.components.tradfri.load_json", - return_value={"mock-host": {"key": "some-info"}}, - ): - assert await async_setup_component(hass, "tradfri", {"tradfri": {}}) - await hass.async_block_till_done() - - config_entry = mock_entry_setup.mock_calls[0][1][1] - assert config_entry.domain == "tradfri" - assert config_entry.source == config_entries.SOURCE_IMPORT - assert config_entry.title == "mock-host" - - async def test_entry_setup_unload(hass, api_factory, gateway_id): """Test config entry setup and unload.""" entry = MockConfigEntry( From 2f5c3c08ef8cde919171d20b9c1f76b85cd80090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 11 Aug 2021 11:27:41 +0200 Subject: [PATCH 147/355] Use monitor name for uptimerobot device registry (#54456) --- homeassistant/components/uptimerobot/entity.py | 2 +- tests/components/uptimerobot/test_init.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/uptimerobot/entity.py b/homeassistant/components/uptimerobot/entity.py index 8ef60b3848b..89ff7680eae 100644 --- a/homeassistant/components/uptimerobot/entity.py +++ b/homeassistant/components/uptimerobot/entity.py @@ -28,7 +28,7 @@ class UptimeRobotEntity(CoordinatorEntity): self._monitor = monitor self._attr_device_info = { "identifiers": {(DOMAIN, str(self.monitor.id))}, - "name": "Uptime Robot", + "name": self.monitor.friendly_name, "manufacturer": "Uptime Robot Team", "entry_type": "service", "model": self.monitor.type.name, diff --git a/tests/components/uptimerobot/test_init.py b/tests/components/uptimerobot/test_init.py index 756831e7615..43f78e7a19f 100644 --- a/tests/components/uptimerobot/test_init.py +++ b/tests/components/uptimerobot/test_init.py @@ -150,6 +150,7 @@ async def test_device_management(hass: HomeAssistant): assert len(devices) == 1 assert devices[0].identifiers == {(DOMAIN, "1234")} + assert devices[0].name == "Test monitor" assert hass.states.get(UPTIMEROBOT_TEST_ENTITY).state == STATE_ON assert hass.states.get(f"{UPTIMEROBOT_TEST_ENTITY}_2") is None From bc417162cf264a5561555f3ce74ad7bdb9bbca69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 11 Aug 2021 12:50:17 +0300 Subject: [PATCH 148/355] Fix Huawei LTE entity state updating (#54447) Since 91a2b96, we no longer key this by the router URL, but the relevant config entry unique id. Closes https://github.com/home-assistant/core/issues/54243 --- homeassistant/components/huawei_lte/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 81b715b71fe..0c545486c82 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -665,9 +665,9 @@ class HuaweiLteBaseEntity(Entity): async_dispatcher_connect(self.hass, UPDATE_SIGNAL, self._async_maybe_update) ) - async def _async_maybe_update(self, url: str) -> None: + async def _async_maybe_update(self, config_entry_unique_id: str) -> None: """Update state if the update signal comes from our router.""" - if url == self.router.url: + if config_entry_unique_id == self.router.config_entry.unique_id: self.async_schedule_update_ha_state(True) async def async_will_remove_from_hass(self) -> None: From 4d9318419725cf9d318848f46c4b2883cd6a63ec Mon Sep 17 00:00:00 2001 From: Felix <0xFelix@users.noreply.github.com> Date: Wed, 11 Aug 2021 12:17:12 +0200 Subject: [PATCH 149/355] Strip attributes whitespace in universal media_player (#54451) When using whitespace in attributes the lookup of the state attribute fails because an entity with whitespace in its name cannot be found. Works: entity_id|state_attribute Does not work: entity_id | state_attribute Fixes #53804 --- homeassistant/components/universal/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 0ebd9eaf890..2e3e6892c1c 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -158,7 +158,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): self._cmds = commands self._attrs = {} for key, val in attributes.items(): - attr = val.split("|", 1) + attr = list(map(str.strip, val.split("|", 1))) if len(attr) == 1: attr.append(None) self._attrs[key] = attr From 4ef9269790170d94b4b8f15f591e9f9f08abfb24 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 11 Aug 2021 12:42:28 +0200 Subject: [PATCH 150/355] Replace prepare_service_call with a simpler fixture in modbus (#53975) * Convert prepare_service_call to a fixture. --- tests/components/modbus/conftest.py | 52 ++++---- tests/components/modbus/test_binary_sensor.py | 35 ++--- tests/components/modbus/test_climate.py | 124 ++++++++++++------ tests/components/modbus/test_cover.py | 90 ++++++------- tests/components/modbus/test_fan.py | 45 ++++--- tests/components/modbus/test_light.py | 45 ++++--- tests/components/modbus/test_sensor.py | 35 ++--- tests/components/modbus/test_switch.py | 45 ++++--- 8 files changed, 253 insertions(+), 218 deletions(-) diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 86eff5e44ad..a33d0932c1d 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -1,4 +1,5 @@ """The tests for the Modbus sensor component.""" +from dataclasses import dataclass from datetime import timedelta import logging from unittest import mock @@ -24,6 +25,16 @@ TEST_MODBUS_NAME = "modbusTest" _LOGGER = logging.getLogger(__name__) +@dataclass +class ReadResult: + """Storage class for register read results.""" + + def __init__(self, register_words): + """Init.""" + self.registers = register_words + self.bits = register_words + + @pytest.fixture def mock_pymodbus(): """Mock pymodbus.""" @@ -59,9 +70,15 @@ async def mock_modbus(hass, caplog, request, do_config): } ] } + mock_pb = mock.MagicMock() with mock.patch( - "homeassistant.components.modbus.modbus.ModbusTcpClient", autospec=True - ) as mock_pb: + "homeassistant.components.modbus.modbus.ModbusTcpClient", return_value=mock_pb + ): + mock_pb.read_coils.return_value = ReadResult([0x00]) + read_result = ReadResult([0x00, 0x00]) + mock_pb.read_discrete_inputs.return_value = read_result + mock_pb.read_input_registers.return_value = read_result + mock_pb.read_holding_registers.return_value = read_result if request.param["testLoad"]: assert await async_setup_component(hass, DOMAIN, config) is True else: @@ -77,14 +94,11 @@ async def mock_test_state(hass, request): return request.param -# dataclass -class ReadResult: - """Storage class for register read results.""" - - def __init__(self, register_words): - """Init.""" - self.registers = register_words - self.bits = register_words +@pytest.fixture +async def mock_ha(hass): + """Load homeassistant to allow service calls.""" + assert await async_setup_component(hass, "homeassistant", {}) + await hass.async_block_till_done() async def base_test( @@ -191,21 +205,3 @@ async def base_test( # Check state entity_id = f"{entity_domain}.{device_name}" return hass.states.get(entity_id).state - - -async def prepare_service_update(hass, config): - """Run test for service write_coil.""" - - config_modbus = { - DOMAIN: { - CONF_NAME: DEFAULT_HUB, - CONF_TYPE: "tcp", - CONF_HOST: "modbusTest", - CONF_PORT: 5001, - **config, - }, - } - assert await async_setup_component(hass, DOMAIN, config_modbus) - await hass.async_block_till_done() - assert await async_setup_component(hass, "homeassistant", {}) - await hass.async_block_till_done() diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index e77fd380a22..dc9a547dc18 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -20,7 +20,7 @@ from homeassistant.const import ( ) from homeassistant.core import State -from .conftest import ReadResult, base_test, prepare_service_update +from .conftest import ReadResult, base_test SENSOR_NAME = "test_binary_sensor" ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" @@ -102,33 +102,34 @@ async def test_all_binary_sensor(hass, do_type, regs, expected): assert state == expected -async def test_service_binary_sensor_update(hass, mock_pymodbus): +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_BINARY_SENSORS: [ + { + CONF_NAME: SENSOR_NAME, + CONF_ADDRESS: 1234, + CONF_INPUT_TYPE: CALL_TYPE_COIL, + } + ] + }, + ], +) +async def test_service_binary_sensor_update(hass, mock_modbus, mock_ha): """Run test for service homeassistant.update_entity.""" - config = { - CONF_BINARY_SENSORS: [ - { - CONF_NAME: SENSOR_NAME, - CONF_ADDRESS: 1234, - CONF_INPUT_TYPE: CALL_TYPE_COIL, - } - ] - } - mock_pymodbus.read_coils.return_value = ReadResult([0x00]) - await prepare_service_update( - hass, - config, - ) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_OFF - mock_pymodbus.read_coils.return_value = ReadResult([0x01]) + mock_modbus.read_coils.return_value = ReadResult([0x01]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ON diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index 97d2c32ba69..71dbb6aa8a7 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -22,7 +22,7 @@ from homeassistant.const import ( ) from homeassistant.core import State -from .conftest import ReadResult, base_test, prepare_service_update +from .conftest import ReadResult, base_test CLIMATE_NAME = "test_climate" ENTITY_ID = f"{CLIMATE_DOMAIN}.{CLIMATE_NAME}" @@ -94,25 +94,24 @@ async def test_temperature_climate(hass, regs, expected): assert state == expected -async def test_service_climate_update(hass, mock_pymodbus): +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_CLIMATES: [ + { + CONF_NAME: CLIMATE_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + } + ] + }, + ], +) +async def test_service_climate_update(hass, mock_modbus, mock_ha): """Run test for service homeassistant.update_entity.""" - - config = { - CONF_CLIMATES: [ - { - CONF_NAME: CLIMATE_NAME, - CONF_TARGET_TEMP: 117, - CONF_ADDRESS: 117, - CONF_SLAVE: 10, - CONF_SCAN_INTERVAL: 0, - } - ] - } - mock_pymodbus.read_input_registers.return_value = ReadResult([0x00]) - await prepare_service_update( - hass, - config, - ) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) @@ -120,34 +119,75 @@ async def test_service_climate_update(hass, mock_pymodbus): @pytest.mark.parametrize( - "data_type, temperature, result", + "temperature, result, do_config", [ - (DATA_TYPE_INT16, 35, [0x00]), - (DATA_TYPE_INT32, 36, [0x00, 0x00]), - (DATA_TYPE_FLOAT32, 37.5, [0x00, 0x00]), - (DATA_TYPE_FLOAT64, "39", [0x00, 0x00, 0x00, 0x00]), + ( + 35, + [0x00], + { + CONF_CLIMATES: [ + { + CONF_NAME: CLIMATE_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_DATA_TYPE: DATA_TYPE_INT16, + } + ] + }, + ), + ( + 36, + [0x00, 0x00], + { + CONF_CLIMATES: [ + { + CONF_NAME: CLIMATE_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_DATA_TYPE: DATA_TYPE_INT32, + } + ] + }, + ), + ( + 37.5, + [0x00, 0x00], + { + CONF_CLIMATES: [ + { + CONF_NAME: CLIMATE_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_DATA_TYPE: DATA_TYPE_FLOAT32, + } + ] + }, + ), + ( + "39", + [0x00, 0x00, 0x00, 0x00], + { + CONF_CLIMATES: [ + { + CONF_NAME: CLIMATE_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_DATA_TYPE: DATA_TYPE_FLOAT64, + } + ] + }, + ), ], ) async def test_service_climate_set_temperature( - hass, data_type, temperature, result, mock_pymodbus + hass, temperature, result, mock_modbus, mock_ha ): - """Run test for service homeassistant.update_entity.""" - config = { - CONF_CLIMATES: [ - { - CONF_NAME: CLIMATE_NAME, - CONF_TARGET_TEMP: 117, - CONF_ADDRESS: 117, - CONF_SLAVE: 10, - CONF_DATA_TYPE: data_type, - } - ] - } - mock_pymodbus.read_holding_registers.return_value = ReadResult(result) - await prepare_service_update( - hass, - config, - ) + """Test set_temperature.""" + mock_modbus.read_holding_registers.return_value = ReadResult(result) await hass.services.async_call( CLIMATE_DOMAIN, "set_temperature", diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py index 8d7e7e39cf8..b1add3e3745 100644 --- a/tests/components/modbus/test_cover.py +++ b/tests/components/modbus/test_cover.py @@ -29,7 +29,7 @@ from homeassistant.const import ( ) from homeassistant.core import State -from .conftest import ReadResult, base_test, prepare_service_update +from .conftest import ReadResult, base_test COVER_NAME = "test_cover" ENTITY_ID = f"{COVER_DOMAIN}.{COVER_NAME}" @@ -158,28 +158,27 @@ async def test_register_cover(hass, regs, expected): assert state == expected -async def test_service_cover_update(hass, mock_pymodbus): +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_COVERS: [ + { + CONF_NAME: COVER_NAME, + CONF_ADDRESS: 1234, + CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, + } + ] + }, + ], +) +async def test_service_cover_update(hass, mock_modbus, mock_ha): """Run test for service homeassistant.update_entity.""" - - config = { - CONF_COVERS: [ - { - CONF_NAME: COVER_NAME, - CONF_ADDRESS: 1234, - CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, - } - ] - } - mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00]) - await prepare_service_update( - hass, - config, - ) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_CLOSED - mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) + mock_modbus.read_holding_registers.return_value = ReadResult([0x01]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) @@ -223,51 +222,52 @@ async def test_restore_state_cover(hass, mock_test_state, mock_modbus): assert hass.states.get(ENTITY_ID).state == test_state -async def test_service_cover_move(hass, mock_pymodbus): +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_COVERS: [ + { + CONF_NAME: COVER_NAME, + CONF_ADDRESS: 1234, + CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_SCAN_INTERVAL: 0, + }, + { + CONF_NAME: f"{COVER_NAME}2", + CONF_INPUT_TYPE: CALL_TYPE_COIL, + CONF_ADDRESS: 1234, + CONF_SCAN_INTERVAL: 0, + }, + ] + }, + ], +) +async def test_service_cover_move(hass, mock_modbus, mock_ha): """Run test for service homeassistant.update_entity.""" ENTITY_ID2 = f"{ENTITY_ID}2" - config = { - CONF_COVERS: [ - { - CONF_NAME: COVER_NAME, - CONF_ADDRESS: 1234, - CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, - CONF_SCAN_INTERVAL: 0, - }, - { - CONF_NAME: f"{COVER_NAME}2", - CONF_INPUT_TYPE: CALL_TYPE_COIL, - CONF_ADDRESS: 1234, - CONF_SCAN_INTERVAL: 0, - }, - ] - } - mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) - await prepare_service_update( - hass, - config, - ) + mock_modbus.read_holding_registers.return_value = ReadResult([0x01]) await hass.services.async_call( "cover", "open_cover", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_OPEN - mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00]) + mock_modbus.read_holding_registers.return_value = ReadResult([0x00]) await hass.services.async_call( "cover", "close_cover", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_CLOSED - mock_pymodbus.reset() - mock_pymodbus.read_holding_registers.side_effect = ModbusException("fail write_") + mock_modbus.reset() + mock_modbus.read_holding_registers.side_effect = ModbusException("fail write_") await hass.services.async_call( "cover", "close_cover", {"entity_id": ENTITY_ID}, blocking=True ) - assert mock_pymodbus.read_holding_registers.called + assert mock_modbus.read_holding_registers.called assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE - mock_pymodbus.read_coils.side_effect = ModbusException("fail write_") + mock_modbus.read_coils.side_effect = ModbusException("fail write_") await hass.services.async_call( "cover", "close_cover", {"entity_id": ENTITY_ID2}, blocking=True ) diff --git a/tests/components/modbus/test_fan.py b/tests/components/modbus/test_fan.py index 13714d6bd0e..e0d23ad48db 100644 --- a/tests/components/modbus/test_fan.py +++ b/tests/components/modbus/test_fan.py @@ -33,7 +33,7 @@ from homeassistant.const import ( from homeassistant.core import State from homeassistant.setup import async_setup_component -from .conftest import ReadResult, base_test, prepare_service_update +from .conftest import ReadResult, base_test FAN_NAME = "test_fan" ENTITY_ID = f"{FAN_DOMAIN}.{FAN_NAME}" @@ -277,30 +277,29 @@ async def test_fan_service_turn(hass, caplog, mock_pymodbus): assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE -async def test_service_fan_update(hass, mock_pymodbus): +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_FANS: [ + { + CONF_NAME: FAN_NAME, + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + CONF_VERIFY: {}, + } + ] + }, + ], +) +async def test_service_fan_update(hass, mock_modbus, mock_ha): """Run test for service homeassistant.update_entity.""" - - config = { - CONF_FANS: [ - { - CONF_NAME: FAN_NAME, - CONF_ADDRESS: 1234, - CONF_WRITE_TYPE: CALL_TYPE_COIL, - CONF_VERIFY: {}, - } - ] - } - mock_pymodbus.read_discrete_inputs.return_value = ReadResult([0x01]) - await prepare_service_update( - hass, - config, - ) - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True - ) - assert hass.states.get(ENTITY_ID).state == STATE_ON - mock_pymodbus.read_coils.return_value = ReadResult([0x00]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_OFF + mock_modbus.read_coils.return_value = ReadResult([0x01]) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True + ) + assert hass.states.get(ENTITY_ID).state == STATE_ON diff --git a/tests/components/modbus/test_light.py b/tests/components/modbus/test_light.py index c7b9b820934..3b3966cdf8a 100644 --- a/tests/components/modbus/test_light.py +++ b/tests/components/modbus/test_light.py @@ -33,7 +33,7 @@ from homeassistant.const import ( from homeassistant.core import State from homeassistant.setup import async_setup_component -from .conftest import ReadResult, base_test, prepare_service_update +from .conftest import ReadResult, base_test LIGHT_NAME = "test_light" ENTITY_ID = f"{LIGHT_DOMAIN}.{LIGHT_NAME}" @@ -277,30 +277,29 @@ async def test_light_service_turn(hass, caplog, mock_pymodbus): assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE -async def test_service_light_update(hass, mock_pymodbus): +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_LIGHTS: [ + { + CONF_NAME: LIGHT_NAME, + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + CONF_VERIFY: {}, + } + ] + }, + ], +) +async def test_service_light_update(hass, mock_modbus, mock_ha): """Run test for service homeassistant.update_entity.""" - - config = { - CONF_LIGHTS: [ - { - CONF_NAME: LIGHT_NAME, - CONF_ADDRESS: 1234, - CONF_WRITE_TYPE: CALL_TYPE_COIL, - CONF_VERIFY: {}, - } - ] - } - mock_pymodbus.read_discrete_inputs.return_value = ReadResult([0x01]) - await prepare_service_update( - hass, - config, - ) - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True - ) - assert hass.states.get(ENTITY_ID).state == STATE_ON - mock_pymodbus.read_coils.return_value = ReadResult([0x00]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_OFF + mock_modbus.read_coils.return_value = ReadResult([0x01]) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True + ) + assert hass.states.get(ENTITY_ID).state == STATE_ON diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index a5ec79d62e4..ef784c9edb6 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -35,7 +35,7 @@ from homeassistant.const import ( ) from homeassistant.core import State -from .conftest import ReadResult, base_test, prepare_service_update +from .conftest import ReadResult, base_test SENSOR_NAME = "test_sensor" ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" @@ -599,27 +599,28 @@ async def test_restore_state_sensor(hass, mock_test_state, mock_modbus): assert hass.states.get(ENTITY_ID).state == mock_test_state[0].state -async def test_service_sensor_update(hass, mock_pymodbus): +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_SENSORS: [ + { + CONF_NAME: SENSOR_NAME, + CONF_ADDRESS: 1234, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, + } + ] + }, + ], +) +async def test_service_sensor_update(hass, mock_modbus, mock_ha): """Run test for service homeassistant.update_entity.""" - config = { - CONF_SENSORS: [ - { - CONF_NAME: SENSOR_NAME, - CONF_ADDRESS: 1234, - CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, - } - ] - } - mock_pymodbus.read_input_registers.return_value = ReadResult([27]) - await prepare_service_update( - hass, - config, - ) + mock_modbus.read_input_registers.return_value = ReadResult([27]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == "27" - mock_pymodbus.read_input_registers.return_value = ReadResult([32]) + mock_modbus.read_input_registers.return_value = ReadResult([32]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py index c620429aad2..48c8ca9e15f 100644 --- a/tests/components/modbus/test_switch.py +++ b/tests/components/modbus/test_switch.py @@ -39,7 +39,7 @@ from homeassistant.core import State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .conftest import ReadResult, base_test, prepare_service_update +from .conftest import ReadResult, base_test from tests.common import async_fire_time_changed @@ -291,33 +291,32 @@ async def test_switch_service_turn(hass, caplog, mock_pymodbus): assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE -async def test_service_switch_update(hass, mock_pymodbus): +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_SWITCHES: [ + { + CONF_NAME: SWITCH_NAME, + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + CONF_VERIFY: {}, + } + ] + }, + ], +) +async def test_service_switch_update(hass, mock_modbus, mock_ha): """Run test for service homeassistant.update_entity.""" - - config = { - CONF_SWITCHES: [ - { - CONF_NAME: SWITCH_NAME, - CONF_ADDRESS: 1234, - CONF_WRITE_TYPE: CALL_TYPE_COIL, - CONF_VERIFY: {}, - } - ] - } - mock_pymodbus.read_discrete_inputs.return_value = ReadResult([0x01]) - await prepare_service_update( - hass, - config, - ) - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True - ) - assert hass.states.get(ENTITY_ID).state == STATE_ON - mock_pymodbus.read_coils.return_value = ReadResult([0x00]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_OFF + mock_modbus.read_coils.return_value = ReadResult([0x01]) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True + ) + assert hass.states.get(ENTITY_ID).state == STATE_ON async def test_delay_switch(hass, mock_pymodbus): From 6285c7775b836b6a6fe4f47037f967c54e7072ce Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 11 Aug 2021 15:56:41 +0200 Subject: [PATCH 151/355] Use SensorEntityDescription and set state class measurement for NUT sensors (#54269) --- homeassistant/components/nut/config_flow.py | 13 +- homeassistant/components/nut/const.py | 805 ++++++++++++++------ homeassistant/components/nut/sensor.py | 47 +- 3 files changed, 586 insertions(+), 279 deletions(-) diff --git a/homeassistant/components/nut/config_flow.py b/homeassistant/components/nut/config_flow.py index 0b5ad8bbc1f..70c097bd6f1 100644 --- a/homeassistant/components/nut/config_flow.py +++ b/homeassistant/components/nut/config_flow.py @@ -25,19 +25,12 @@ from .const import ( DOMAIN, KEY_STATUS, KEY_STATUS_DISPLAY, - SENSOR_NAME, SENSOR_TYPES, ) _LOGGER = logging.getLogger(__name__) -SENSOR_DICT = { - sensor_id: sensor_spec[SENSOR_NAME] - for sensor_id, sensor_spec in SENSOR_TYPES.items() -} - - def _base_schema(discovery_info): """Generate base schema.""" base_schema = {} @@ -59,15 +52,15 @@ def _resource_schema_base(available_resources, selected_resources): """Resource selection schema.""" known_available_resources = { - sensor_id: sensor[SENSOR_NAME] - for sensor_id, sensor in SENSOR_TYPES.items() + sensor_id: sensor_desc.name + for sensor_id, sensor_desc in SENSOR_TYPES.items() if sensor_id in available_resources } if KEY_STATUS in known_available_resources: known_available_resources[KEY_STATUS_DISPLAY] = SENSOR_TYPES[ KEY_STATUS_DISPLAY - ][SENSOR_NAME] + ].name return { vol.Required(CONF_RESOURCES, default=selected_resources): cv.multi_select( diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index 1f5fecdd219..b48121eeaf8 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -1,10 +1,17 @@ """The nut component.""" + +from __future__ import annotations + +from typing import Final + from homeassistant.components.sensor import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + SensorEntityDescription, ) from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, @@ -40,246 +47,559 @@ PYNUT_MODEL = "model" PYNUT_FIRMWARE = "firmware" PYNUT_NAME = "name" -SENSOR_TYPES = { - "ups.status.display": ["Status", "", "mdi:information-outline", None], - "ups.status": ["Status Data", "", "mdi:information-outline", None], - "ups.alarm": ["Alarms", "", "mdi:alarm", None], - "ups.temperature": [ - "UPS Temperature", - TEMP_CELSIUS, - None, - DEVICE_CLASS_TEMPERATURE, - ], - "ups.load": ["Load", PERCENTAGE, "mdi:gauge", None], - "ups.load.high": ["Overload Setting", PERCENTAGE, "mdi:gauge", None], - "ups.id": ["System identifier", "", "mdi:information-outline", None], - "ups.delay.start": ["Load Restart Delay", TIME_SECONDS, "mdi:timer-outline", None], - "ups.delay.reboot": ["UPS Reboot Delay", TIME_SECONDS, "mdi:timer-outline", None], - "ups.delay.shutdown": [ - "UPS Shutdown Delay", - TIME_SECONDS, - "mdi:timer-outline", - None, - ], - "ups.timer.start": ["Load Start Timer", TIME_SECONDS, "mdi:timer-outline", None], - "ups.timer.reboot": ["Load Reboot Timer", TIME_SECONDS, "mdi:timer-outline", None], - "ups.timer.shutdown": [ - "Load Shutdown Timer", - TIME_SECONDS, - "mdi:timer-outline", - None, - ], - "ups.test.interval": [ - "Self-Test Interval", - TIME_SECONDS, - "mdi:timer-outline", - None, - ], - "ups.test.result": ["Self-Test Result", "", "mdi:information-outline", None], - "ups.test.date": ["Self-Test Date", "", "mdi:calendar", None], - "ups.display.language": ["Language", "", "mdi:information-outline", None], - "ups.contacts": ["External Contacts", "", "mdi:information-outline", None], - "ups.efficiency": ["Efficiency", PERCENTAGE, "mdi:gauge", None], - "ups.power": ["Current Apparent Power", POWER_VOLT_AMPERE, "mdi:flash", None], - "ups.power.nominal": ["Nominal Power", POWER_VOLT_AMPERE, "mdi:flash", None], - "ups.realpower": [ - "Current Real Power", - POWER_WATT, - None, - DEVICE_CLASS_POWER, - ], - "ups.realpower.nominal": [ - "Nominal Real Power", - POWER_WATT, - None, - DEVICE_CLASS_POWER, - ], - "ups.beeper.status": ["Beeper Status", "", "mdi:information-outline", None], - "ups.type": ["UPS Type", "", "mdi:information-outline", None], - "ups.watchdog.status": ["Watchdog Status", "", "mdi:information-outline", None], - "ups.start.auto": ["Start on AC", "", "mdi:information-outline", None], - "ups.start.battery": ["Start on Battery", "", "mdi:information-outline", None], - "ups.start.reboot": ["Reboot on Battery", "", "mdi:information-outline", None], - "ups.shutdown": ["Shutdown Ability", "", "mdi:information-outline", None], - "battery.charge": [ - "Battery Charge", - PERCENTAGE, - None, - DEVICE_CLASS_BATTERY, - ], - "battery.charge.low": ["Low Battery Setpoint", PERCENTAGE, "mdi:gauge", None], - "battery.charge.restart": [ - "Minimum Battery to Start", - PERCENTAGE, - "mdi:gauge", - None, - ], - "battery.charge.warning": [ - "Warning Battery Setpoint", - PERCENTAGE, - "mdi:gauge", - None, - ], - "battery.charger.status": ["Charging Status", "", "mdi:information-outline", None], - "battery.voltage": [ - "Battery Voltage", - ELECTRIC_POTENTIAL_VOLT, - None, - DEVICE_CLASS_VOLTAGE, - ], - "battery.voltage.nominal": [ - "Nominal Battery Voltage", - ELECTRIC_POTENTIAL_VOLT, - None, - DEVICE_CLASS_VOLTAGE, - ], - "battery.voltage.low": [ - "Low Battery Voltage", - ELECTRIC_POTENTIAL_VOLT, - None, - DEVICE_CLASS_VOLTAGE, - ], - "battery.voltage.high": [ - "High Battery Voltage", - ELECTRIC_POTENTIAL_VOLT, - None, - DEVICE_CLASS_VOLTAGE, - ], - "battery.capacity": ["Battery Capacity", "Ah", "mdi:flash", None], - "battery.current": [ - "Battery Current", - ELECTRIC_CURRENT_AMPERE, - "mdi:flash", - None, - ], - "battery.current.total": [ - "Total Battery Current", - ELECTRIC_CURRENT_AMPERE, - "mdi:flash", - None, - ], - "battery.temperature": [ - "Battery Temperature", - TEMP_CELSIUS, - None, - DEVICE_CLASS_TEMPERATURE, - ], - "battery.runtime": ["Battery Runtime", TIME_SECONDS, "mdi:timer-outline", None], - "battery.runtime.low": [ - "Low Battery Runtime", - TIME_SECONDS, - "mdi:timer-outline", - None, - ], - "battery.runtime.restart": [ - "Minimum Battery Runtime to Start", - TIME_SECONDS, - "mdi:timer-outline", - None, - ], - "battery.alarm.threshold": [ - "Battery Alarm Threshold", - "", - "mdi:information-outline", - None, - ], - "battery.date": ["Battery Date", "", "mdi:calendar", None], - "battery.mfr.date": ["Battery Manuf. Date", "", "mdi:calendar", None], - "battery.packs": ["Number of Batteries", "", "mdi:information-outline", None], - "battery.packs.bad": [ - "Number of Bad Batteries", - "", - "mdi:information-outline", - None, - ], - "battery.type": ["Battery Chemistry", "", "mdi:information-outline", None], - "input.sensitivity": [ - "Input Power Sensitivity", - "", - "mdi:information-outline", - None, - ], - "input.transfer.low": [ - "Low Voltage Transfer", - ELECTRIC_POTENTIAL_VOLT, - None, - DEVICE_CLASS_VOLTAGE, - ], - "input.transfer.high": [ - "High Voltage Transfer", - ELECTRIC_POTENTIAL_VOLT, - None, - DEVICE_CLASS_VOLTAGE, - ], - "input.transfer.reason": [ - "Voltage Transfer Reason", - "", - "mdi:information-outline", - None, - ], - "input.voltage": [ - "Input Voltage", - ELECTRIC_POTENTIAL_VOLT, - None, - DEVICE_CLASS_VOLTAGE, - ], - "input.voltage.nominal": [ - "Nominal Input Voltage", - ELECTRIC_POTENTIAL_VOLT, - None, - DEVICE_CLASS_VOLTAGE, - ], - "input.frequency": ["Input Line Frequency", FREQUENCY_HERTZ, "mdi:flash", None], - "input.frequency.nominal": [ - "Nominal Input Line Frequency", - FREQUENCY_HERTZ, - "mdi:flash", - None, - ], - "input.frequency.status": [ - "Input Frequency Status", - "", - "mdi:information-outline", - None, - ], - "output.current": ["Output Current", ELECTRIC_CURRENT_AMPERE, "mdi:flash", None], - "output.current.nominal": [ - "Nominal Output Current", - ELECTRIC_CURRENT_AMPERE, - "mdi:flash", - None, - ], - "output.voltage": [ - "Output Voltage", - ELECTRIC_POTENTIAL_VOLT, - None, - DEVICE_CLASS_VOLTAGE, - ], - "output.voltage.nominal": [ - "Nominal Output Voltage", - ELECTRIC_POTENTIAL_VOLT, - None, - DEVICE_CLASS_VOLTAGE, - ], - "output.frequency": ["Output Frequency", FREQUENCY_HERTZ, "mdi:flash", None], - "output.frequency.nominal": [ - "Nominal Output Frequency", - FREQUENCY_HERTZ, - "mdi:flash", - None, - ], - "ambient.humidity": [ - "Ambient Humidity", - PERCENTAGE, - None, - DEVICE_CLASS_HUMIDITY, - ], - "ambient.temperature": [ - "Ambient Temperature", - TEMP_CELSIUS, - None, - DEVICE_CLASS_TEMPERATURE, - ], +SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { + "ups.status.display": SensorEntityDescription( + key="ups.status.display", + name="Status", + unit_of_measurement=None, + icon="mdi:information-outline", + device_class=None, + state_class=None, + ), + "ups.status": SensorEntityDescription( + key="ups.status", + name="Status Data", + unit_of_measurement=None, + icon="mdi:information-outline", + device_class=None, + state_class=None, + ), + "ups.alarm": SensorEntityDescription( + key="ups.alarm", + name="Alarms", + unit_of_measurement=None, + icon="mdi:alarm", + device_class=None, + state_class=None, + ), + "ups.temperature": SensorEntityDescription( + key="ups.temperature", + name="UPS Temperature", + unit_of_measurement=TEMP_CELSIUS, + icon=None, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "ups.load": SensorEntityDescription( + key="ups.load", + name="Load", + unit_of_measurement=PERCENTAGE, + icon="mdi:gauge", + device_class=None, + state_class=STATE_CLASS_MEASUREMENT, + ), + "ups.load.high": SensorEntityDescription( + key="ups.load.high", + name="Overload Setting", + unit_of_measurement=PERCENTAGE, + icon="mdi:gauge", + device_class=None, + state_class=None, + ), + "ups.id": SensorEntityDescription( + key="ups.id", + name="System identifier", + unit_of_measurement=None, + icon="mdi:information-outline", + device_class=None, + state_class=None, + ), + "ups.delay.start": SensorEntityDescription( + key="ups.delay.start", + name="Load Restart Delay", + unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", + device_class=None, + state_class=None, + ), + "ups.delay.reboot": SensorEntityDescription( + key="ups.delay.reboot", + name="UPS Reboot Delay", + unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", + device_class=None, + state_class=None, + ), + "ups.delay.shutdown": SensorEntityDescription( + key="ups.delay.shutdown", + name="UPS Shutdown Delay", + unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", + device_class=None, + state_class=None, + ), + "ups.timer.start": SensorEntityDescription( + key="ups.timer.start", + name="Load Start Timer", + unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", + device_class=None, + state_class=None, + ), + "ups.timer.reboot": SensorEntityDescription( + key="ups.timer.reboot", + name="Load Reboot Timer", + unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", + device_class=None, + state_class=None, + ), + "ups.timer.shutdown": SensorEntityDescription( + key="ups.timer.shutdown", + name="Load Shutdown Timer", + unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", + device_class=None, + state_class=None, + ), + "ups.test.interval": SensorEntityDescription( + key="ups.test.interval", + name="Self-Test Interval", + unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", + device_class=None, + state_class=None, + ), + "ups.test.result": SensorEntityDescription( + key="ups.test.result", + name="Self-Test Result", + unit_of_measurement=None, + icon="mdi:information-outline", + device_class=None, + state_class=None, + ), + "ups.test.date": SensorEntityDescription( + key="ups.test.date", + name="Self-Test Date", + unit_of_measurement=None, + icon="mdi:calendar", + device_class=None, + state_class=None, + ), + "ups.display.language": SensorEntityDescription( + key="ups.display.language", + name="Language", + unit_of_measurement=None, + icon="mdi:information-outline", + device_class=None, + state_class=None, + ), + "ups.contacts": SensorEntityDescription( + key="ups.contacts", + name="External Contacts", + unit_of_measurement=None, + icon="mdi:information-outline", + device_class=None, + state_class=None, + ), + "ups.efficiency": SensorEntityDescription( + key="ups.efficiency", + name="Efficiency", + unit_of_measurement=PERCENTAGE, + icon="mdi:gauge", + device_class=None, + state_class=STATE_CLASS_MEASUREMENT, + ), + "ups.power": SensorEntityDescription( + key="ups.power", + name="Current Apparent Power", + unit_of_measurement=POWER_VOLT_AMPERE, + icon="mdi:flash", + device_class=None, + state_class=STATE_CLASS_MEASUREMENT, + ), + "ups.power.nominal": SensorEntityDescription( + key="ups.power.nominal", + name="Nominal Power", + unit_of_measurement=POWER_VOLT_AMPERE, + icon="mdi:flash", + device_class=None, + state_class=None, + ), + "ups.realpower": SensorEntityDescription( + key="ups.realpower", + name="Current Real Power", + unit_of_measurement=POWER_WATT, + icon=None, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + "ups.realpower.nominal": SensorEntityDescription( + key="ups.realpower.nominal", + name="Nominal Real Power", + unit_of_measurement=POWER_WATT, + icon=None, + device_class=DEVICE_CLASS_POWER, + state_class=None, + ), + "ups.beeper.status": SensorEntityDescription( + key="ups.beeper.status", + name="Beeper Status", + unit_of_measurement=None, + icon="mdi:information-outline", + device_class=None, + state_class=None, + ), + "ups.type": SensorEntityDescription( + key="ups.type", + name="UPS Type", + unit_of_measurement=None, + icon="mdi:information-outline", + device_class=None, + state_class=None, + ), + "ups.watchdog.status": SensorEntityDescription( + key="ups.watchdog.status", + name="Watchdog Status", + unit_of_measurement=None, + icon="mdi:information-outline", + device_class=None, + state_class=None, + ), + "ups.start.auto": SensorEntityDescription( + key="ups.start.auto", + name="Start on AC", + unit_of_measurement=None, + icon="mdi:information-outline", + device_class=None, + state_class=None, + ), + "ups.start.battery": SensorEntityDescription( + key="ups.start.battery", + name="Start on Battery", + unit_of_measurement=None, + icon="mdi:information-outline", + device_class=None, + state_class=None, + ), + "ups.start.reboot": SensorEntityDescription( + key="ups.start.reboot", + name="Reboot on Battery", + unit_of_measurement=None, + icon="mdi:information-outline", + device_class=None, + state_class=None, + ), + "ups.shutdown": SensorEntityDescription( + key="ups.shutdown", + name="Shutdown Ability", + unit_of_measurement=None, + icon="mdi:information-outline", + device_class=None, + state_class=None, + ), + "battery.charge": SensorEntityDescription( + key="battery.charge", + name="Battery Charge", + unit_of_measurement=PERCENTAGE, + icon=None, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + ), + "battery.charge.low": SensorEntityDescription( + key="battery.charge.low", + name="Low Battery Setpoint", + unit_of_measurement=PERCENTAGE, + icon="mdi:gauge", + device_class=None, + state_class=None, + ), + "battery.charge.restart": SensorEntityDescription( + key="battery.charge.restart", + name="Minimum Battery to Start", + unit_of_measurement=PERCENTAGE, + icon="mdi:gauge", + device_class=None, + state_class=None, + ), + "battery.charge.warning": SensorEntityDescription( + key="battery.charge.warning", + name="Warning Battery Setpoint", + unit_of_measurement=PERCENTAGE, + icon="mdi:gauge", + device_class=None, + state_class=None, + ), + "battery.charger.status": SensorEntityDescription( + key="battery.charger.status", + name="Charging Status", + unit_of_measurement=None, + icon="mdi:information-outline", + device_class=None, + state_class=None, + ), + "battery.voltage": SensorEntityDescription( + key="battery.voltage", + name="Battery Voltage", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + icon=None, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "battery.voltage.nominal": SensorEntityDescription( + key="battery.voltage.nominal", + name="Nominal Battery Voltage", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + icon=None, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=None, + ), + "battery.voltage.low": SensorEntityDescription( + key="battery.voltage.low", + name="Low Battery Voltage", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + icon=None, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=None, + ), + "battery.voltage.high": SensorEntityDescription( + key="battery.voltage.high", + name="High Battery Voltage", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + icon=None, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=None, + ), + "battery.capacity": SensorEntityDescription( + key="battery.capacity", + name="Battery Capacity", + unit_of_measurement="Ah", + icon="mdi:flash", + device_class=None, + state_class=None, + ), + "battery.current": SensorEntityDescription( + key="battery.current", + name="Battery Current", + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + icon="mdi:flash", + device_class=None, + state_class=STATE_CLASS_MEASUREMENT, + ), + "battery.current.total": SensorEntityDescription( + key="battery.current.total", + name="Total Battery Current", + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + icon="mdi:flash", + device_class=None, + state_class=None, + ), + "battery.temperature": SensorEntityDescription( + key="battery.temperature", + name="Battery Temperature", + unit_of_measurement=TEMP_CELSIUS, + icon=None, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "battery.runtime": SensorEntityDescription( + key="battery.runtime", + name="Battery Runtime", + unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", + device_class=None, + state_class=None, + ), + "battery.runtime.low": SensorEntityDescription( + key="battery.runtime.low", + name="Low Battery Runtime", + unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", + device_class=None, + state_class=None, + ), + "battery.runtime.restart": SensorEntityDescription( + key="battery.runtime.restart", + name="Minimum Battery Runtime to Start", + unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", + device_class=None, + state_class=None, + ), + "battery.alarm.threshold": SensorEntityDescription( + key="battery.alarm.threshold", + name="Battery Alarm Threshold", + unit_of_measurement=None, + icon="mdi:information-outline", + device_class=None, + state_class=None, + ), + "battery.date": SensorEntityDescription( + key="battery.date", + name="Battery Date", + unit_of_measurement=None, + icon="mdi:calendar", + device_class=None, + state_class=None, + ), + "battery.mfr.date": SensorEntityDescription( + key="battery.mfr.date", + name="Battery Manuf. Date", + unit_of_measurement=None, + icon="mdi:calendar", + device_class=None, + state_class=None, + ), + "battery.packs": SensorEntityDescription( + key="battery.packs", + name="Number of Batteries", + unit_of_measurement=None, + icon="mdi:information-outline", + device_class=None, + state_class=None, + ), + "battery.packs.bad": SensorEntityDescription( + key="battery.packs.bad", + name="Number of Bad Batteries", + unit_of_measurement=None, + icon="mdi:information-outline", + device_class=None, + state_class=None, + ), + "battery.type": SensorEntityDescription( + key="battery.type", + name="Battery Chemistry", + unit_of_measurement=None, + icon="mdi:information-outline", + device_class=None, + state_class=None, + ), + "input.sensitivity": SensorEntityDescription( + key="input.sensitivity", + name="Input Power Sensitivity", + unit_of_measurement=None, + icon="mdi:information-outline", + device_class=None, + state_class=None, + ), + "input.transfer.low": SensorEntityDescription( + key="input.transfer.low", + name="Low Voltage Transfer", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + icon=None, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=None, + ), + "input.transfer.high": SensorEntityDescription( + key="input.transfer.high", + name="High Voltage Transfer", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + icon=None, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=None, + ), + "input.transfer.reason": SensorEntityDescription( + key="input.transfer.reason", + name="Voltage Transfer Reason", + unit_of_measurement=None, + icon="mdi:information-outline", + device_class=None, + state_class=None, + ), + "input.voltage": SensorEntityDescription( + key="input.voltage", + name="Input Voltage", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + icon=None, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "input.voltage.nominal": SensorEntityDescription( + key="input.voltage.nominal", + name="Nominal Input Voltage", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + icon=None, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=None, + ), + "input.frequency": SensorEntityDescription( + key="input.frequency", + name="Input Line Frequency", + unit_of_measurement=FREQUENCY_HERTZ, + icon="mdi:flash", + device_class=None, + state_class=STATE_CLASS_MEASUREMENT, + ), + "input.frequency.nominal": SensorEntityDescription( + key="input.frequency.nominal", + name="Nominal Input Line Frequency", + unit_of_measurement=FREQUENCY_HERTZ, + icon="mdi:flash", + device_class=None, + state_class=None, + ), + "input.frequency.status": SensorEntityDescription( + key="input.frequency.status", + name="Input Frequency Status", + unit_of_measurement=None, + icon="mdi:information-outline", + device_class=None, + state_class=None, + ), + "output.current": SensorEntityDescription( + key="output.current", + name="Output Current", + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + icon="mdi:flash", + device_class=None, + state_class=STATE_CLASS_MEASUREMENT, + ), + "output.current.nominal": SensorEntityDescription( + key="output.current.nominal", + name="Nominal Output Current", + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + icon="mdi:flash", + device_class=None, + state_class=None, + ), + "output.voltage": SensorEntityDescription( + key="output.voltage", + name="Output Voltage", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + icon=None, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "output.voltage.nominal": SensorEntityDescription( + key="output.voltage.nominal", + name="Nominal Output Voltage", + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + icon=None, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=None, + ), + "output.frequency": SensorEntityDescription( + key="output.frequency", + name="Output Frequency", + unit_of_measurement=FREQUENCY_HERTZ, + icon="mdi:flash", + device_class=None, + state_class=STATE_CLASS_MEASUREMENT, + ), + "output.frequency.nominal": SensorEntityDescription( + key="output.frequency.nominal", + name="Nominal Output Frequency", + unit_of_measurement=FREQUENCY_HERTZ, + icon="mdi:flash", + device_class=None, + state_class=None, + ), + "ambient.humidity": SensorEntityDescription( + key="ambient.humidity", + name="Ambient Humidity", + unit_of_measurement=PERCENTAGE, + icon=None, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + "ambient.temperature": SensorEntityDescription( + key="ambient.temperature", + name="Ambient Temperature", + unit_of_measurement=TEMP_CELSIUS, + icon=None, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), } STATE_TYPES = { @@ -299,8 +619,3 @@ STATE_TYPES = { "FSD": "Forced Shutdown", "ALARM": "Alarm", } - -SENSOR_NAME = 0 -SENSOR_UNIT = 1 -SENSOR_ICON = 2 -SENSOR_DEVICE_CLASS = 3 diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 1eb67e45aa5..456778c3ca5 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -1,9 +1,15 @@ """Provides a sensor to track various status aspects of a UPS.""" +from __future__ import annotations + import logging -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.nut import PyNUTData +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import ATTR_STATE, CONF_RESOURCES, STATE_UNKNOWN -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import ( COORDINATOR, @@ -16,11 +22,7 @@ from .const import ( PYNUT_MODEL, PYNUT_NAME, PYNUT_UNIQUE_ID, - SENSOR_DEVICE_CLASS, - SENSOR_ICON, - SENSOR_NAME, SENSOR_TYPES, - SENSOR_UNIT, STATE_TYPES, ) @@ -60,7 +62,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): coordinator, data, name.title(), - sensor_type, + SENSOR_TYPES[sensor_type], unique_id, manufacturer, model, @@ -82,18 +84,18 @@ class NUTSensor(CoordinatorEntity, SensorEntity): def __init__( self, - coordinator, - data, - name, - sensor_type, - unique_id, - manufacturer, - model, - firmware, - ): + coordinator: DataUpdateCoordinator, + data: PyNUTData, + name: str, + sensor_description: SensorEntityDescription, + unique_id: str, + manufacturer: str | None, + model: str | None, + firmware: str | None, + ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._type = sensor_type + self.entity_description = sensor_description self._manufacturer = manufacturer self._firmware = firmware self._model = model @@ -101,10 +103,7 @@ class NUTSensor(CoordinatorEntity, SensorEntity): self._data = data self._unique_id = unique_id - self._attr_device_class = SENSOR_TYPES[self._type][SENSOR_DEVICE_CLASS] - self._attr_icon = SENSOR_TYPES[self._type][SENSOR_ICON] - self._attr_name = f"{name} {SENSOR_TYPES[sensor_type][SENSOR_NAME]}" - self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][SENSOR_UNIT] + self._attr_name = f"{name} {sensor_description.name}" @property def device_info(self): @@ -128,16 +127,16 @@ class NUTSensor(CoordinatorEntity, SensorEntity): """Sensor Unique id.""" if not self._unique_id: return None - return f"{self._unique_id}_{self._type}" + return f"{self._unique_id}_{self.entity_description.key}" @property def state(self): """Return entity state from ups.""" if not self._data.status: return None - if self._type == KEY_STATUS_DISPLAY: + if self.entity_description.key == KEY_STATUS_DISPLAY: return _format_display_state(self._data.status) - return self._data.status.get(self._type) + return self._data.status.get(self.entity_description.key) @property def extra_state_attributes(self): From b1fc05413abf61449e2d12e4162332352b84c67c Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 11 Aug 2021 10:13:38 -0400 Subject: [PATCH 152/355] Bump notifications-android-tv to 0.1.3 (#54462) Co-authored-by: Martin Hjelmare --- homeassistant/components/nfandroidtv/__init__.py | 7 +------ homeassistant/components/nfandroidtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/nfandroidtv/__init__.py b/homeassistant/components/nfandroidtv/__init__.py index 90a76c1c747..35aecdb6916 100644 --- a/homeassistant/components/nfandroidtv/__init__.py +++ b/homeassistant/components/nfandroidtv/__init__.py @@ -1,6 +1,4 @@ """The NFAndroidTV integration.""" -import logging - from notifications_android_tv.notifications import ConnectError, Notifications from homeassistant.components.notify import DOMAIN as NOTIFY @@ -12,8 +10,6 @@ from homeassistant.helpers import discovery from .const import DOMAIN -_LOGGER = logging.getLogger(__name__) - PLATFORMS = [NOTIFY] @@ -41,8 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await hass.async_add_executor_job(Notifications, host) except ConnectError as ex: - _LOGGER.warning("Failed to connect: %s", ex) - raise ConfigEntryNotReady from ex + raise ConfigEntryNotReady("Failed to connect") from ex hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { diff --git a/homeassistant/components/nfandroidtv/manifest.json b/homeassistant/components/nfandroidtv/manifest.json index 5516f144fd4..c1dea03aa09 100644 --- a/homeassistant/components/nfandroidtv/manifest.json +++ b/homeassistant/components/nfandroidtv/manifest.json @@ -2,7 +2,7 @@ "domain": "nfandroidtv", "name": "Notifications for Android TV / Fire TV", "documentation": "https://www.home-assistant.io/integrations/nfandroidtv", - "requirements": ["notifications-android-tv==0.1.2"], + "requirements": ["notifications-android-tv==0.1.3"], "codeowners": ["@tkdrob"], "config_flow": true, "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 8fe9b1c6d57..76c2a9a1170 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1041,7 +1041,7 @@ niluclient==0.1.2 noaa-coops==0.1.8 # homeassistant.components.nfandroidtv -notifications-android-tv==0.1.2 +notifications-android-tv==0.1.3 # homeassistant.components.notify_events notify-events==1.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67caf01a0e4..cf1c915d751 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -582,7 +582,7 @@ nettigo-air-monitor==1.0.0 nexia==0.9.11 # homeassistant.components.nfandroidtv -notifications-android-tv==0.1.2 +notifications-android-tv==0.1.3 # homeassistant.components.notify_events notify-events==1.0.4 From cb26f334c35767b40a1f58cbe273a5e477a721c0 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Wed, 11 Aug 2021 15:34:36 +0100 Subject: [PATCH 153/355] Use pycarwings2 2.11 (#54424) --- 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 298343d2d8d..55cd28d59fa 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.10"], + "requirements": ["pycarwings2==2.11"], "codeowners": ["@filcole"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 76c2a9a1170..bbf9e8344f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1345,7 +1345,7 @@ pyblackbird==0.5 pybotvac==0.0.22 # homeassistant.components.nissan_leaf -pycarwings2==2.10 +pycarwings2==2.11 # homeassistant.components.cloudflare pycfdns==1.2.1 From 13c34d646f5c4cb186b4170fb60630da2602229c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 11 Aug 2021 17:09:16 +0200 Subject: [PATCH 154/355] Remove empty currency from discovery info (#54478) --- homeassistant/components/api/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 0a11cf04651..a91d8540286 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -43,7 +43,6 @@ from homeassistant.helpers.system_info import async_get_system_info _LOGGER = logging.getLogger(__name__) ATTR_BASE_URL = "base_url" -ATTR_CURRENCY = "currency" ATTR_EXTERNAL_URL = "external_url" ATTR_INTERNAL_URL = "internal_url" ATTR_LOCATION_NAME = "location_name" @@ -196,7 +195,6 @@ class APIDiscoveryView(HomeAssistantView): # always needs authentication ATTR_REQUIRES_API_PASSWORD: True, ATTR_VERSION: __version__, - ATTR_CURRENCY: None, } with suppress(NoURLAvailableError): From 1e14b3a0ac0e50ed388362671c71afe7ea5d449a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Aug 2021 10:12:46 -0500 Subject: [PATCH 155/355] Ensure camera handles non-jpeg image sources correctly (#54474) --- homeassistant/components/camera/__init__.py | 3 +-- homeassistant/components/demo/camera.py | 13 +++++++--- homeassistant/components/demo/demo_0.png | Bin 0 -> 224227 bytes homeassistant/components/demo/demo_1.png | Bin 0 -> 232693 bytes homeassistant/components/demo/demo_2.png | Bin 0 -> 231077 bytes homeassistant/components/demo/demo_3.png | Bin 0 -> 232693 bytes tests/components/camera/test_init.py | 27 ++++++++++++++++++-- 7 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/demo/demo_0.png create mode 100644 homeassistant/components/demo/demo_1.png create mode 100644 homeassistant/components/demo/demo_2.png create mode 100644 homeassistant/components/demo/demo_3.png diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index c6cada2e3c9..14cd64df920 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -177,8 +177,7 @@ async def _async_get_image( if ( width is not None and height is not None - and "jpeg" in content_type - or "jpg" in content_type + and ("jpeg" in content_type or "jpg" in content_type) ): assert width is not None assert height is not None diff --git a/homeassistant/components/demo/camera.py b/homeassistant/components/demo/camera.py index b3f9b505aee..572a5bf331e 100644 --- a/homeassistant/components/demo/camera.py +++ b/homeassistant/components/demo/camera.py @@ -8,7 +8,12 @@ from homeassistant.components.camera import SUPPORT_ON_OFF, Camera async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Demo camera platform.""" - async_add_entities([DemoCamera("Demo camera")]) + async_add_entities( + [ + DemoCamera("Demo camera", "image/jpg"), + DemoCamera("Demo camera png", "image/png"), + ] + ) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -19,10 +24,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DemoCamera(Camera): """The representation of a Demo camera.""" - def __init__(self, name): + def __init__(self, name, content_type): """Initialize demo camera component.""" super().__init__() self._name = name + self.content_type = content_type self._motion_status = False self.is_streaming = True self._images_index = 0 @@ -32,7 +38,8 @@ class DemoCamera(Camera): ) -> bytes: """Return a faked still image response.""" self._images_index = (self._images_index + 1) % 4 - image_path = Path(__file__).parent / f"demo_{self._images_index}.jpg" + ext = "jpg" if self.content_type == "image/jpg" else "png" + image_path = Path(__file__).parent / f"demo_{self._images_index}.{ext}" return await self.hass.async_add_executor_job(image_path.read_bytes) diff --git a/homeassistant/components/demo/demo_0.png b/homeassistant/components/demo/demo_0.png new file mode 100644 index 0000000000000000000000000000000000000000..f45852e3b20f9a3b42238e89814ccdf8b1a63d37 GIT binary patch literal 224227 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

o{(Zh!ADw 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
  • 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

    CmE^$)X$Hpq)&T7Uq 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 O7(8A5T-G@yGywoX3(_$F literal 0 HcmV?d00001 diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index f4267de234c..bb3f76e0d1b 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -118,10 +118,33 @@ async def test_get_image_from_camera_with_width_height_scaled(hass, image_mock_u ) assert mock_camera.called - assert image.content_type == "image/jpeg" + assert image.content_type == "image/jpg" assert image.content == EMPTY_8_6_JPEG +async def test_get_image_from_camera_not_jpeg(hass, image_mock_url): + """Grab an image from camera entity that we cannot scale.""" + + turbo_jpeg = mock_turbo_jpeg( + first_width=16, first_height=12, second_width=300, second_height=200 + ) + with patch( + "homeassistant.components.camera.img_util.TurboJPEGSingleton.instance", + return_value=turbo_jpeg, + ), patch( + "homeassistant.components.demo.camera.Path.read_bytes", + autospec=True, + return_value=b"png", + ) as mock_camera: + image = await camera.async_get_image( + hass, "camera.demo_camera_png", width=4, height=3 + ) + + assert mock_camera.called + assert image.content_type == "image/png" + assert image.content == b"png" + + async def test_get_stream_source_from_camera(hass, mock_camera): """Fetch stream source from camera entity.""" @@ -200,7 +223,7 @@ async def test_websocket_camera_thumbnail(hass, hass_ws_client, mock_camera): assert msg["id"] == 5 assert msg["type"] == TYPE_RESULT assert msg["success"] - assert msg["result"]["content_type"] == "image/jpeg" + assert msg["result"]["content_type"] == "image/jpg" assert msg["result"]["content"] == base64.b64encode(b"Test").decode("utf-8") From 028a3d3e53e1652b0425cdd2eacfbab4b4846b51 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 11 Aug 2021 17:19:02 +0200 Subject: [PATCH 156/355] Complete mysensors sensor coverage (#54471) --- .coveragerc | 1 - tests/components/mysensors/conftest.py | 93 ++++++++++-- tests/components/mysensors/test_sensor.py | 136 +++++++++++++++++- .../mysensors/distance_sensor_state.json | 22 +++ .../mysensors/energy_sensor_state.json | 21 +++ .../mysensors/sound_sensor_state.json | 21 +++ .../mysensors/temperature_sensor_state.json | 21 +++ 7 files changed, 301 insertions(+), 14 deletions(-) create mode 100644 tests/fixtures/mysensors/distance_sensor_state.json create mode 100644 tests/fixtures/mysensors/energy_sensor_state.json create mode 100644 tests/fixtures/mysensors/sound_sensor_state.json create mode 100644 tests/fixtures/mysensors/temperature_sensor_state.json diff --git a/.coveragerc b/.coveragerc index 8088bbece78..bf51d5ad594 100644 --- a/.coveragerc +++ b/.coveragerc @@ -666,7 +666,6 @@ omit = homeassistant/components/mysensors/helpers.py homeassistant/components/mysensors/light.py homeassistant/components/mysensors/notify.py - homeassistant/components/mysensors/sensor.py homeassistant/components/mysensors/switch.py homeassistant/components/mystrom/binary_sensor.py homeassistant/components/mystrom/light.py diff --git a/tests/components/mysensors/conftest.py b/tests/components/mysensors/conftest.py index 49c32301442..1843e495801 100644 --- a/tests/components/mysensors/conftest.py +++ b/tests/components/mysensors/conftest.py @@ -3,13 +3,14 @@ from __future__ import annotations from collections.abc import AsyncGenerator, Generator import json -from typing import Any +from typing import Any, Callable from unittest.mock import MagicMock, patch from mysensors.persistence import MySensorsJSONDecoder from mysensors.sensor import Sensor import pytest +from homeassistant.components.device_tracker.legacy import Device from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.components.mysensors import CONF_VERSION, DEFAULT_BAUD_RATE from homeassistant.components.mysensors.const import ( @@ -27,14 +28,14 @@ from tests.common import MockConfigEntry, load_fixture @pytest.fixture(autouse=True) -def device_tracker_storage(mock_device_tracker_conf): +def device_tracker_storage(mock_device_tracker_conf: list[Device]) -> list[Device]: """Mock out device tracker known devices storage.""" devices = mock_device_tracker_conf return devices @pytest.fixture(name="mqtt") -def mock_mqtt_fixture(hass) -> None: +def mock_mqtt_fixture(hass: HomeAssistant) -> None: """Mock the MQTT integration.""" hass.config.components.add(MQTT_DOMAIN) @@ -75,14 +76,14 @@ def mock_gateway_features( ) -> None: """Mock the gateway features.""" - async def mock_start_persistence(): + async def mock_start_persistence() -> None: """Load nodes from via persistence.""" gateway = transport_class.call_args[0][0] gateway.sensors.update(nodes) tasks.start_persistence.side_effect = mock_start_persistence - async def mock_start(): + async def mock_start() -> None: """Mock the start method.""" gateway = transport_class.call_args[0][0] gateway.on_conn_made(gateway) @@ -97,7 +98,7 @@ def transport_fixture(serial_transport: MagicMock) -> MagicMock: @pytest.fixture(name="serial_entry") -async def serial_entry_fixture(hass) -> MockConfigEntry: +async def serial_entry_fixture(hass: HomeAssistant) -> MockConfigEntry: """Create a config entry for a serial gateway.""" entry = MockConfigEntry( domain=DOMAIN, @@ -120,15 +121,25 @@ def config_entry_fixture(serial_entry: MockConfigEntry) -> MockConfigEntry: @pytest.fixture async def integration( hass: HomeAssistant, transport: MagicMock, config_entry: MockConfigEntry -) -> AsyncGenerator[MockConfigEntry, None]: +) -> AsyncGenerator[tuple[MockConfigEntry, Callable[[str], None]], None]: """Set up the mysensors integration with a config entry.""" device = config_entry.data[CONF_DEVICE] config: dict[str, Any] = {DOMAIN: {CONF_GATEWAYS: [{CONF_DEVICE: device}]}} config_entry.add_to_hass(hass) + + def receive_message(message_string: str) -> None: + """Receive a message with the transport. + + The message_string parameter is a string in the MySensors message format. + """ + gateway = transport.call_args[0][0] + # node_id;child_id;command;ack;type;payload\n + gateway.logic(message_string) + with patch("homeassistant.components.mysensors.device.UPDATE_DELAY", new=0): await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() - yield config_entry + yield config_entry, receive_message def load_nodes_state(fixture_path: str) -> dict: @@ -151,7 +162,7 @@ def gps_sensor_state_fixture() -> dict: @pytest.fixture -def gps_sensor(gateway_nodes, gps_sensor_state) -> Sensor: +def gps_sensor(gateway_nodes: dict[int, Sensor], gps_sensor_state: dict) -> Sensor: """Load the gps sensor.""" nodes = update_gateway_nodes(gateway_nodes, gps_sensor_state) node = nodes[1] @@ -165,8 +176,70 @@ def power_sensor_state_fixture() -> dict: @pytest.fixture -def power_sensor(gateway_nodes, power_sensor_state) -> Sensor: +def power_sensor(gateway_nodes: dict[int, Sensor], power_sensor_state: dict) -> Sensor: """Load the power sensor.""" nodes = update_gateway_nodes(gateway_nodes, power_sensor_state) node = nodes[1] return node + + +@pytest.fixture(name="energy_sensor_state", scope="session") +def energy_sensor_state_fixture() -> dict: + """Load the energy sensor state.""" + return load_nodes_state("mysensors/energy_sensor_state.json") + + +@pytest.fixture +def energy_sensor( + gateway_nodes: dict[int, Sensor], energy_sensor_state: dict +) -> Sensor: + """Load the energy sensor.""" + nodes = update_gateway_nodes(gateway_nodes, energy_sensor_state) + node = nodes[1] + return node + + +@pytest.fixture(name="sound_sensor_state", scope="session") +def sound_sensor_state_fixture() -> dict: + """Load the sound sensor state.""" + return load_nodes_state("mysensors/sound_sensor_state.json") + + +@pytest.fixture +def sound_sensor(gateway_nodes: dict[int, Sensor], sound_sensor_state: dict) -> Sensor: + """Load the sound sensor.""" + nodes = update_gateway_nodes(gateway_nodes, sound_sensor_state) + node = nodes[1] + return node + + +@pytest.fixture(name="distance_sensor_state", scope="session") +def distance_sensor_state_fixture() -> dict: + """Load the distance sensor state.""" + return load_nodes_state("mysensors/distance_sensor_state.json") + + +@pytest.fixture +def distance_sensor( + gateway_nodes: dict[int, Sensor], distance_sensor_state: dict +) -> Sensor: + """Load the distance sensor.""" + nodes = update_gateway_nodes(gateway_nodes, distance_sensor_state) + node = nodes[1] + return node + + +@pytest.fixture(name="temperature_sensor_state", scope="session") +def temperature_sensor_state_fixture() -> dict: + """Load the temperature sensor state.""" + return load_nodes_state("mysensors/temperature_sensor_state.json") + + +@pytest.fixture +def temperature_sensor( + gateway_nodes: dict[int, Sensor], temperature_sensor_state: dict +) -> Sensor: + """Load the temperature sensor.""" + nodes = update_gateway_nodes(gateway_nodes, temperature_sensor_state) + node = nodes[1] + return node diff --git a/tests/components/mysensors/test_sensor.py b/tests/components/mysensors/test_sensor.py index 6edddc68592..880226ced60 100644 --- a/tests/components/mysensors/test_sensor.py +++ b/tests/components/mysensors/test_sensor.py @@ -1,31 +1,161 @@ """Provide tests for mysensors sensor platform.""" +from __future__ import annotations +from typing import Callable -from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT +from mysensors.sensor import Sensor +import pytest + +from homeassistant.components.sensor import ( + ATTR_LAST_RESET, + ATTR_STATE_CLASS, + STATE_CLASS_MEASUREMENT, +) 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, + TEMP_FAHRENHEIT, ) +from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utc_from_timestamp +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem + +from tests.common import MockConfigEntry -async def test_gps_sensor(hass, gps_sensor, integration): +async def test_gps_sensor( + hass: HomeAssistant, + gps_sensor: Sensor, + integration: tuple[MockConfigEntry, Callable[[str], None]], +) -> None: """Test a gps sensor.""" entity_id = "sensor.gps_sensor_1_1" + _, receive_message = integration state = hass.states.get(entity_id) + assert state assert state.state == "40.741894,-73.989311,12" + altitude = 0 + new_coords = "40.782,-73.965" + message_string = f"1;1;1;0;49;{new_coords},{altitude}\n" -async def test_power_sensor(hass, power_sensor, integration): + receive_message(message_string) + # the integration adds multiple jobs to do the update currently + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == f"{new_coords},{altitude}" + + +async def test_power_sensor( + hass: HomeAssistant, + power_sensor: Sensor, + integration: tuple[MockConfigEntry, Callable[[str], None]], +) -> None: """Test a power sensor.""" entity_id = "sensor.power_sensor_1_1" state = hass.states.get(entity_id) + assert state assert state.state == "1200" assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_POWER assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + assert ATTR_LAST_RESET not in state.attributes + + +async def test_energy_sensor( + hass: HomeAssistant, + energy_sensor: Sensor, + integration: tuple[MockConfigEntry, Callable[[str], None]], +) -> None: + """Test an energy sensor.""" + entity_id = "sensor.energy_sensor_1_1" + + state = hass.states.get(entity_id) + + assert state + assert state.state == "18000" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_KILO_WATT_HOUR + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + assert state.attributes[ATTR_LAST_RESET] == utc_from_timestamp(0).isoformat() + + +async def test_sound_sensor( + hass: HomeAssistant, + sound_sensor: Sensor, + integration: tuple[MockConfigEntry, Callable[[str], None]], +) -> None: + """Test a sound sensor.""" + entity_id = "sensor.sound_sensor_1_1" + + state = hass.states.get(entity_id) + + assert state + assert state.state == "10" + assert state.attributes[ATTR_ICON] == "mdi:volume-high" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "dB" + + +async def test_distance_sensor( + hass: HomeAssistant, + distance_sensor: Sensor, + integration: tuple[MockConfigEntry, Callable[[str], None]], +) -> None: + """Test a distance sensor.""" + entity_id = "sensor.distance_sensor_1_1" + + state = hass.states.get(entity_id) + + assert state + assert state.state == "15" + assert state.attributes[ATTR_ICON] == "mdi:ruler" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "cm" + + +@pytest.mark.parametrize( + "unit_system, unit", + [(METRIC_SYSTEM, TEMP_CELSIUS), (IMPERIAL_SYSTEM, TEMP_FAHRENHEIT)], +) +async def test_temperature_sensor( + hass: HomeAssistant, + temperature_sensor: Sensor, + integration: tuple[MockConfigEntry, Callable[[str], None]], + unit_system: UnitSystem, + unit: str, +) -> None: + """Test a temperature sensor.""" + entity_id = "sensor.temperature_sensor_1_1" + hass.config.units = unit_system + _, receive_message = integration + temperature = "22.0" + message_string = f"1;1;1;0;0;{temperature}\n" + + receive_message(message_string) + # the integration adds multiple jobs to do the update currently + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == temperature + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == unit + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT diff --git a/tests/fixtures/mysensors/distance_sensor_state.json b/tests/fixtures/mysensors/distance_sensor_state.json new file mode 100644 index 00000000000..ff8b246b880 --- /dev/null +++ b/tests/fixtures/mysensors/distance_sensor_state.json @@ -0,0 +1,22 @@ +{ + "1": { + "sensor_id": 1, + "children": { + "1": { + "id": 1, + "type": 15, + "description": "", + "values": { + "13": "15", + "43": "cm" + } + } + }, + "type": 17, + "sketch_name": "Distance Sensor", + "sketch_version": "1.0", + "battery_level": 0, + "protocol_version": "2.3.2", + "heartbeat": 0 + } +} diff --git a/tests/fixtures/mysensors/energy_sensor_state.json b/tests/fixtures/mysensors/energy_sensor_state.json new file mode 100644 index 00000000000..063083c9c1e --- /dev/null +++ b/tests/fixtures/mysensors/energy_sensor_state.json @@ -0,0 +1,21 @@ +{ + "1": { + "sensor_id": 1, + "children": { + "1": { + "id": 1, + "type": 13, + "description": "", + "values": { + "18": "18000" + } + } + }, + "type": 17, + "sketch_name": "Energy Sensor", + "sketch_version": "1.0", + "battery_level": 0, + "protocol_version": "2.3.2", + "heartbeat": 0 + } +} diff --git a/tests/fixtures/mysensors/sound_sensor_state.json b/tests/fixtures/mysensors/sound_sensor_state.json new file mode 100644 index 00000000000..35651243250 --- /dev/null +++ b/tests/fixtures/mysensors/sound_sensor_state.json @@ -0,0 +1,21 @@ +{ + "1": { + "sensor_id": 1, + "children": { + "1": { + "id": 1, + "type": 33, + "description": "", + "values": { + "37": "10" + } + } + }, + "type": 17, + "sketch_name": "Sound Sensor", + "sketch_version": "1.0", + "battery_level": 0, + "protocol_version": "2.3.2", + "heartbeat": 0 + } +} diff --git a/tests/fixtures/mysensors/temperature_sensor_state.json b/tests/fixtures/mysensors/temperature_sensor_state.json new file mode 100644 index 00000000000..4367be6a3cd --- /dev/null +++ b/tests/fixtures/mysensors/temperature_sensor_state.json @@ -0,0 +1,21 @@ +{ + "1": { + "sensor_id": 1, + "children": { + "1": { + "id": 1, + "type": 6, + "description": "", + "values": { + "0": "20.0" + } + } + }, + "type": 17, + "sketch_name": "Temperature Sensor", + "sketch_version": "1.0", + "battery_level": 0, + "protocol_version": "2.3.2", + "heartbeat": 0 + } +} From 6f70302901b16764da8160bfd8faa104a3d402cb Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 11 Aug 2021 17:57:56 +0200 Subject: [PATCH 157/355] Fix arlo platform schema (#54470) --- homeassistant/components/arlo/sensor.py | 30 +++++++++++++++++++------ 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/arlo/sensor.py b/homeassistant/components/arlo/sensor.py index d17668ae721..cc08cd133e4 100644 --- a/homeassistant/components/arlo/sensor.py +++ b/homeassistant/components/arlo/sensor.py @@ -1,4 +1,6 @@ """Sensor support for Netgear Arlo IP cameras.""" +from __future__ import annotations + from dataclasses import replace import logging @@ -28,11 +30,21 @@ from . import ATTRIBUTION, DATA_ARLO, DEFAULT_BRAND, SIGNAL_UPDATE_ARLO _LOGGER = logging.getLogger(__name__) -SENSOR_TYPES = ( - SensorEntityDescription(key="last_capture", name="Last", icon="mdi:run-fast"), - SensorEntityDescription(key="total_cameras", name="Arlo Cameras", icon="mdi:video"), +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( - key="captured_today", name="Captured Today", icon="mdi:file-video" + key="last_capture", + name="Last", + icon="mdi:run-fast", + ), + SensorEntityDescription( + key="total_cameras", + name="Arlo Cameras", + icon="mdi:video", + ), + SensorEntityDescription( + key="captured_today", + name="Captured Today", + icon="mdi:file-video", ), SensorEntityDescription( key="battery_level", @@ -41,7 +53,9 @@ SENSOR_TYPES = ( device_class=DEVICE_CLASS_BATTERY, ), SensorEntityDescription( - key="signal_strength", name="Signal Strength", icon="mdi:signal" + key="signal_strength", + name="Signal Strength", + icon="mdi:signal", ), SensorEntityDescription( key="temperature", @@ -63,10 +77,12 @@ SENSOR_TYPES = ( ), ) +SENSOR_KEYS = [desc.key for desc in SENSOR_TYPES] + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] + vol.Required(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( + cv.ensure_list, [vol.In(SENSOR_KEYS)] ) } ) From d6483f2f36ab3febe9565d25209dca63cb09d0e5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 11 Aug 2021 18:01:45 +0200 Subject: [PATCH 158/355] Upgrade isort to 5.9.3 (#54481) --- .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 b31a9cef116..38ba2a503af 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: - --configfile=tests/bandit.yaml files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/isort - rev: 5.8.0 + rev: 5.9.3 hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 19d55b1255c..e89785c25a8 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -7,7 +7,7 @@ flake8-comprehensions==3.5.0 flake8-docstrings==1.6.0 flake8-noqa==1.1.0 flake8==3.9.2 -isort==5.8.0 +isort==5.9.3 mccabe==0.6.1 pycodestyle==2.7.0 pydocstyle==6.0.0 From 98a4e6a7e8bf29fc8e17be57423ad590300dc177 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 11 Aug 2021 09:12:49 -0700 Subject: [PATCH 159/355] Fix possible unhandled IQVIA exception with allergy outlook data (#54477) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen --- homeassistant/components/iqvia/sensor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index 0ff236a8f79..5762e4a3888 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -104,7 +104,7 @@ class ForecastSensor(IQVIAEntity): @callback def update_from_latest_data(self): """Update the sensor.""" - if not self.coordinator.data: + if not self.available: return data = self.coordinator.data.get("Location", {}) @@ -120,6 +120,7 @@ class ForecastSensor(IQVIAEntity): if i["minimum"] <= average <= i["maximum"] ] + self._attr_state = average self._attr_extra_state_attributes.update( { ATTR_CITY: data["City"].title(), @@ -134,6 +135,10 @@ class ForecastSensor(IQVIAEntity): outlook_coordinator = self.hass.data[DOMAIN][DATA_COORDINATOR][ self._entry.entry_id ][TYPE_ALLERGY_OUTLOOK] + + if not outlook_coordinator.last_update_success: + return + self._attr_extra_state_attributes[ ATTR_OUTLOOK ] = outlook_coordinator.data.get("Outlook") @@ -141,8 +146,6 @@ class ForecastSensor(IQVIAEntity): ATTR_SEASON ] = outlook_coordinator.data.get("Season") - self._attr_state = average - class IndexSensor(IQVIAEntity): """Define sensor related to indices.""" From f020d65416177e4647a846ec017f441fe08c0696 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 11 Aug 2021 18:49:56 +0200 Subject: [PATCH 160/355] Add battery support to energy (#54432) --- homeassistant/components/energy/data.py | 19 ++++++++++++++++++- tests/components/energy/test_websocket_api.py | 5 +++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/energy/data.py b/homeassistant/components/energy/data.py index c053dea4741..9196694953a 100644 --- a/homeassistant/components/energy/data.py +++ b/homeassistant/components/energy/data.py @@ -79,7 +79,16 @@ class SolarSourceType(TypedDict): config_entry_solar_forecast: list[str] | None -SourceType = Union[GridSourceType, SolarSourceType] +class BatterySourceType(TypedDict): + """Dictionary holding the source of battery storage.""" + + type: Literal["battery"] + + stat_energy_from: str + stat_energy_to: str + + +SourceType = Union[GridSourceType, SolarSourceType, BatterySourceType] class DeviceConsumption(TypedDict): @@ -177,6 +186,13 @@ SOLAR_SOURCE_SCHEMA = vol.Schema( vol.Optional("config_entry_solar_forecast"): vol.Any([str], None), } ) +BATTERY_SOURCE_SCHEMA = vol.Schema( + { + vol.Required("type"): "battery", + vol.Required("stat_energy_from"): str, + vol.Required("stat_energy_to"): str, + } +) def check_type_limits(value: list[SourceType]) -> list[SourceType]: @@ -197,6 +213,7 @@ ENERGY_SOURCE_SCHEMA = vol.All( { "grid": GRID_SOURCE_SCHEMA, "solar": SOLAR_SOURCE_SCHEMA, + "battery": BATTERY_SOURCE_SCHEMA, }, ) ] diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index a14a8d0986e..60ac82108bc 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -104,6 +104,11 @@ async def test_save_preferences(hass, hass_ws_client, hass_storage) -> None: "stat_energy_from": "my_solar_production", "config_entry_solar_forecast": ["predicted_config_entry"], }, + { + "type": "battery", + "stat_energy_from": "my_battery_draining", + "stat_energy_to": "my_battery_charging", + }, ], "device_consumption": [{"stat_consumption": "some_device_usage"}], } From 41f3c2766c41fc79b269de83baea62dd0d18a9b9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Aug 2021 18:57:12 +0200 Subject: [PATCH 161/355] Move temperature conversions to entity base class (2/8) (#54468) --- homeassistant/components/daikin/sensor.py | 8 +- .../components/danfoss_air/sensor.py | 4 +- homeassistant/components/darksky/sensor.py | 6 +- homeassistant/components/deconz/sensor.py | 14 +- homeassistant/components/delijn/sensor.py | 2 +- homeassistant/components/deluge/sensor.py | 4 +- homeassistant/components/demo/sensor.py | 4 +- homeassistant/components/derivative/sensor.py | 4 +- .../components/deutsche_bahn/sensor.py | 2 +- .../components/devolo_home_control/sensor.py | 8 +- homeassistant/components/dexcom/sensor.py | 6 +- homeassistant/components/dht/sensor.py | 4 +- homeassistant/components/discogs/sensor.py | 4 +- homeassistant/components/dnsip/sensor.py | 4 +- homeassistant/components/dovado/sensor.py | 4 +- homeassistant/components/dsmr/sensor.py | 4 +- .../components/dsmr_reader/definitions.py | 130 +++++++++--------- .../components/dsmr_reader/sensor.py | 4 +- .../components/dte_energy_bridge/sensor.py | 4 +- .../components/dublin_bus_transport/sensor.py | 4 +- .../components/dwd_weather_warnings/sensor.py | 2 +- homeassistant/components/dweet/sensor.py | 4 +- homeassistant/components/dyson/sensor.py | 18 +-- homeassistant/components/eafm/sensor.py | 4 +- homeassistant/components/ebox/sensor.py | 28 ++-- homeassistant/components/ebusd/sensor.py | 4 +- .../components/ecoal_boiler/sensor.py | 4 +- homeassistant/components/ecobee/sensor.py | 4 +- homeassistant/components/econet/sensor.py | 4 +- .../eddystone_temperature/sensor.py | 4 +- homeassistant/components/edl21/sensor.py | 4 +- homeassistant/components/efergy/sensor.py | 4 +- .../components/eight_sleep/sensor.py | 12 +- homeassistant/components/eliqonline/sensor.py | 4 +- homeassistant/components/elkm1/sensor.py | 6 +- homeassistant/components/emoncms/sensor.py | 4 +- homeassistant/components/emonitor/sensor.py | 4 +- homeassistant/components/energy/sensor.py | 6 +- homeassistant/components/enocean/sensor.py | 4 +- .../components/enphase_envoy/const.py | 18 +-- .../components/enphase_envoy/sensor.py | 2 +- .../entur_public_transport/sensor.py | 4 +- .../components/environment_canada/sensor.py | 4 +- homeassistant/components/envirophat/sensor.py | 4 +- homeassistant/components/envisalink/sensor.py | 2 +- .../components/epsonworkforce/sensor.py | 14 +- homeassistant/components/esphome/sensor.py | 6 +- homeassistant/components/essent/sensor.py | 4 +- homeassistant/components/etherscan/sensor.py | 4 +- homeassistant/components/ezviz/sensor.py | 2 +- 50 files changed, 207 insertions(+), 205 deletions(-) diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py index 3bfc0a3926c..0defa633387 100644 --- a/homeassistant/components/daikin/sensor.py +++ b/homeassistant/components/daikin/sensor.py @@ -86,7 +86,7 @@ class DaikinSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" raise NotImplementedError @@ -101,7 +101,7 @@ class DaikinSensor(SensorEntity): return self._sensor.get(CONF_ICON) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._sensor[CONF_UNIT_OF_MEASUREMENT] @@ -119,7 +119,7 @@ class DaikinClimateSensor(DaikinSensor): """Representation of a Climate Sensor.""" @property - def state(self): + def native_value(self): """Return the internal state of the sensor.""" if self._device_attribute == ATTR_INSIDE_TEMPERATURE: return self._api.device.inside_temperature @@ -141,7 +141,7 @@ class DaikinPowerSensor(DaikinSensor): """Representation of a power/energy consumption sensor.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._device_attribute == ATTR_TOTAL_POWER: return round(self._api.device.current_total_power_consumption, 2) diff --git a/homeassistant/components/danfoss_air/sensor.py b/homeassistant/components/danfoss_air/sensor.py index 792a95e8ac4..25db56a1624 100644 --- a/homeassistant/components/danfoss_air/sensor.py +++ b/homeassistant/components/danfoss_air/sensor.py @@ -100,12 +100,12 @@ class DanfossAir(SensorEntity): return self._device_class @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index 058969d96f9..e73d9b2e1be 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -574,12 +574,12 @@ class DarkSkySensor(SensorEntity): return f"{self.client_name} {self._name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement @@ -730,7 +730,7 @@ class DarkSkyAlertSensor(SensorEntity): return f"{self.client_name} {self._name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 9282f2d26cc..a741a2d37c1 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -160,7 +160,9 @@ class DeconzSensor(DeconzDevice, SensorEntity): self._attr_device_class = DEVICE_CLASS.get(type(self._device)) self._attr_icon = ICON.get(type(self._device)) self._attr_state_class = STATE_CLASS.get(type(self._device)) - self._attr_unit_of_measurement = UNIT_OF_MEASUREMENT.get(type(self._device)) + self._attr_native_unit_of_measurement = UNIT_OF_MEASUREMENT.get( + type(self._device) + ) if device.type in Consumption.ZHATYPE: self._attr_last_reset = dt_util.utc_from_timestamp(0) @@ -173,7 +175,7 @@ class DeconzSensor(DeconzDevice, SensorEntity): super().async_update_callback(force_update=force_update) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.state @@ -217,7 +219,7 @@ class DeconzTemperature(DeconzDevice, SensorEntity): _attr_device_class = DEVICE_CLASS_TEMPERATURE _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_unit_of_measurement = TEMP_CELSIUS + _attr_native_unit_of_measurement = TEMP_CELSIUS TYPE = DOMAIN @@ -240,7 +242,7 @@ class DeconzTemperature(DeconzDevice, SensorEntity): super().async_update_callback(force_update=force_update) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.secondary_temperature @@ -250,7 +252,7 @@ class DeconzBattery(DeconzDevice, SensorEntity): _attr_device_class = DEVICE_CLASS_BATTERY _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE TYPE = DOMAIN @@ -284,7 +286,7 @@ class DeconzBattery(DeconzDevice, SensorEntity): return f"{self.serial}-battery" @property - def state(self): + def native_value(self): """Return the state of the battery.""" return self._device.battery diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py index b105ff5ff7b..395c2d93dff 100644 --- a/homeassistant/components/delijn/sensor.py +++ b/homeassistant/components/delijn/sensor.py @@ -112,7 +112,7 @@ class DeLijnPublicTransportSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index 0c79e6f835e..63c9645dac4 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -87,7 +87,7 @@ class DelugeSensor(SensorEntity): return f"{self.client_name} {self._name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -97,7 +97,7 @@ class DelugeSensor(SensorEntity): return self._available @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index 488c34be983..21ec8e1d391 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -120,10 +120,10 @@ class DemoSensor(SensorEntity): """Initialize the sensor.""" self._attr_device_class = device_class self._attr_name = name - self._attr_state = state + self._attr_native_unit_of_measurement = unit_of_measurement + self._attr_native_value = state self._attr_state_class = state_class self._attr_unique_id = unique_id - self._attr_unit_of_measurement = unit_of_measurement self._attr_device_info = { "identifiers": {(DOMAIN, unique_id)}, diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index c8b639a1db1..45f5db57a90 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -196,12 +196,12 @@ class DerivativeSensor(RestoreEntity, SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return round(self._state, self._round_digits) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/deutsche_bahn/sensor.py b/homeassistant/components/deutsche_bahn/sensor.py index 33fd9a8224f..34711a9a2d7 100644 --- a/homeassistant/components/deutsche_bahn/sensor.py +++ b/homeassistant/components/deutsche_bahn/sensor.py @@ -59,7 +59,7 @@ class DeutscheBahnSensor(SensorEntity): return ICON @property - def state(self): + def native_value(self): """Return the departure time of the next train.""" return self._state diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index 7cb8cc8e837..5c8bed7818b 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -78,7 +78,7 @@ class DevoloMultiLevelDeviceEntity(DevoloDeviceEntity, SensorEntity): """Abstract representation of a multi level sensor within devolo Home Control.""" @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" return self._value @@ -106,7 +106,7 @@ class DevoloGenericMultiLevelDeviceEntity(DevoloMultiLevelDeviceEntity): self._attr_device_class = DEVICE_CLASS_MAPPING.get( self._multi_level_sensor_property.sensor_type ) - self._attr_unit_of_measurement = self._multi_level_sensor_property.unit + self._attr_native_unit_of_measurement = self._multi_level_sensor_property.unit self._value = self._multi_level_sensor_property.value @@ -132,7 +132,7 @@ class DevoloBatteryEntity(DevoloMultiLevelDeviceEntity): ) self._attr_device_class = DEVICE_CLASS_MAPPING.get("battery") - self._attr_unit_of_measurement = PERCENTAGE + self._attr_native_unit_of_measurement = PERCENTAGE self._value = device_instance.battery_level @@ -157,7 +157,7 @@ class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity): self._sensor_type = consumption self._attr_device_class = DEVICE_CLASS_MAPPING.get(consumption) - self._attr_unit_of_measurement = getattr( + self._attr_native_unit_of_measurement = getattr( device_instance.consumption_property[element_uid], f"{consumption}_unit" ) diff --git a/homeassistant/components/dexcom/sensor.py b/homeassistant/components/dexcom/sensor.py index 730a1824e1a..316f36e3630 100644 --- a/homeassistant/components/dexcom/sensor.py +++ b/homeassistant/components/dexcom/sensor.py @@ -42,12 +42,12 @@ class DexcomGlucoseValueSensor(CoordinatorEntity, SensorEntity): return GLUCOSE_VALUE_ICON @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of the device.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.coordinator.data: return getattr(self.coordinator.data, self._attribute_unit_of_measurement) @@ -82,7 +82,7 @@ class DexcomGlucoseTrendSensor(CoordinatorEntity, SensorEntity): return GLUCOSE_TREND_ICON[0] @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.coordinator.data: return self.coordinator.data.trend_description diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py index 1300c165b37..d81d12f33cf 100644 --- a/homeassistant/components/dht/sensor.py +++ b/homeassistant/components/dht/sensor.py @@ -134,12 +134,12 @@ class DHTSensor(SensorEntity): return f"{self.client_name} {self._name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/discogs/sensor.py b/homeassistant/components/discogs/sensor.py index 81beec0e60e..3d90956a2b5 100644 --- a/homeassistant/components/discogs/sensor.py +++ b/homeassistant/components/discogs/sensor.py @@ -105,7 +105,7 @@ class DiscogsSensor(SensorEntity): return f"{self._name} {SENSORS[self._type]['name']}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -115,7 +115,7 @@ class DiscogsSensor(SensorEntity): return SENSORS[self._type]["icon"] @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return SENSORS[self._type]["unit_of_measurement"] diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index 2fb0e30da90..a429d336379 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -79,6 +79,6 @@ class WanIpSensor(SensorEntity): response = None if response: - self._attr_state = response[0].host + self._attr_native_value = response[0].host else: - self._attr_state = None + self._attr_native_value = None diff --git a/homeassistant/components/dovado/sensor.py b/homeassistant/components/dovado/sensor.py index e7b3dbdd363..46f4c34cc31 100644 --- a/homeassistant/components/dovado/sensor.py +++ b/homeassistant/components/dovado/sensor.py @@ -89,7 +89,7 @@ class DovadoSensor(SensorEntity): return f"{self._data.name} {SENSORS[self._sensor][1]}" @property - def state(self): + def native_value(self): """Return the sensor state.""" return self._state @@ -99,7 +99,7 @@ class DovadoSensor(SensorEntity): return SENSORS[self._sensor][3] @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return SENSORS[self._sensor][2] diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index faff62ddeb4..ae3fb6b01a4 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -238,7 +238,7 @@ class DSMREntity(SensorEntity): return attr @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of sensor, if available, translate if needed.""" value = self.get_dsmr_object_attr("value") if value is None: @@ -258,7 +258,7 @@ class DSMREntity(SensorEntity): return None @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" return self.get_dsmr_object_attr("unit") diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index 1a46f86132b..a5fc2b8147a 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -50,7 +50,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/reading/electricity_delivered_1", name="Low tariff usage", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, last_reset=dt_util.utc_from_timestamp(0), ), @@ -58,7 +58,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/reading/electricity_returned_1", name="Low tariff returned", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, last_reset=dt_util.utc_from_timestamp(0), ), @@ -66,7 +66,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/reading/electricity_delivered_2", name="High tariff usage", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, last_reset=dt_util.utc_from_timestamp(0), ), @@ -74,7 +74,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/reading/electricity_returned_2", name="High tariff returned", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, last_reset=dt_util.utc_from_timestamp(0), ), @@ -82,14 +82,14 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/reading/electricity_currently_delivered", name="Current power usage", device_class=DEVICE_CLASS_POWER, - unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_KILO_WATT, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/electricity_currently_returned", name="Current power return", device_class=DEVICE_CLASS_POWER, - unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_KILO_WATT, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -97,7 +97,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Current power usage L1", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_POWER, - unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_KILO_WATT, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -105,7 +105,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Current power usage L2", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_POWER, - unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_KILO_WATT, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -113,7 +113,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Current power usage L3", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_POWER, - unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_KILO_WATT, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -121,7 +121,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Current power return L1", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_POWER, - unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_KILO_WATT, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -129,7 +129,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Current power return L2", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_POWER, - unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_KILO_WATT, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -137,7 +137,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Current power return L3", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_POWER, - unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_KILO_WATT, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -145,7 +145,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Gas meter usage", entity_registry_enabled_default=False, icon="mdi:fire", - unit_of_measurement=VOLUME_CUBIC_METERS, + native_unit_of_measurement=VOLUME_CUBIC_METERS, state_class=STATE_CLASS_MEASUREMENT, last_reset=dt_util.utc_from_timestamp(0), ), @@ -154,7 +154,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Current voltage L1", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_VOLTAGE, - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -162,7 +162,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Current voltage L2", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_VOLTAGE, - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -170,7 +170,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Current voltage L3", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_VOLTAGE, - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -178,7 +178,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Phase power current L1", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_CURRENT, - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -186,7 +186,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Phase power current L2", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_CURRENT, - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -194,7 +194,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Phase power current L3", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_CURRENT, - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -207,7 +207,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/consumption/gas/delivered", name="Gas usage", icon="mdi:fire", - unit_of_measurement=VOLUME_CUBIC_METERS, + native_unit_of_measurement=VOLUME_CUBIC_METERS, state_class=STATE_CLASS_MEASUREMENT, last_reset=dt_util.utc_from_timestamp(0), ), @@ -215,7 +215,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/consumption/gas/currently_delivered", name="Current gas usage", icon="mdi:fire", - unit_of_measurement=VOLUME_CUBIC_METERS, + native_unit_of_measurement=VOLUME_CUBIC_METERS, state_class=STATE_CLASS_MEASUREMENT, ), DSMRReaderSensorEntityDescription( @@ -228,7 +228,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/day-consumption/electricity1", name="Low tariff usage", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, last_reset=dt_util.utc_from_timestamp(0), ), @@ -236,7 +236,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/day-consumption/electricity2", name="High tariff usage", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, last_reset=dt_util.utc_from_timestamp(0), ), @@ -244,7 +244,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/day-consumption/electricity1_returned", name="Low tariff return", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, last_reset=dt_util.utc_from_timestamp(0), ), @@ -252,7 +252,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/day-consumption/electricity2_returned", name="High tariff return", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, last_reset=dt_util.utc_from_timestamp(0), ), @@ -260,7 +260,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/day-consumption/electricity_merged", name="Power usage total", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, last_reset=dt_util.utc_from_timestamp(0), ), @@ -268,7 +268,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/day-consumption/electricity_returned_merged", name="Power return total", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, last_reset=dt_util.utc_from_timestamp(0), ), @@ -276,73 +276,73 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/day-consumption/electricity1_cost", name="Low tariff cost", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity2_cost", name="High tariff cost", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity_cost_merged", name="Power total cost", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/gas", name="Gas usage", icon="mdi:counter", - unit_of_measurement=VOLUME_CUBIC_METERS, + native_unit_of_measurement=VOLUME_CUBIC_METERS, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/gas_cost", name="Gas cost", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/total_cost", name="Total cost", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/energy_supplier_price_electricity_delivered_1", name="Low tariff delivered price", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/energy_supplier_price_electricity_delivered_2", name="High tariff delivered price", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/energy_supplier_price_electricity_returned_1", name="Low tariff returned price", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/energy_supplier_price_electricity_returned_2", name="High tariff returned price", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/energy_supplier_price_gas", name="Gas price", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/fixed_cost", name="Current day fixed cost", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/meter-stats/dsmr_version", @@ -415,156 +415,156 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/current-month/electricity1", name="Current month low tariff usage", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + 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, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + 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, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + 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, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + 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, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + 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, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), DSMRReaderSensorEntityDescription( key="dsmr/current-month/electricity1_cost", name="Current month low tariff cost", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/current-month/electricity2_cost", name="Current month high tariff cost", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/current-month/electricity_cost_merged", name="Current month power total cost", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/current-month/gas", name="Current month gas usage", icon="mdi:counter", - unit_of_measurement=VOLUME_CUBIC_METERS, + native_unit_of_measurement=VOLUME_CUBIC_METERS, ), DSMRReaderSensorEntityDescription( key="dsmr/current-month/gas_cost", name="Current month gas cost", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/current-month/fixed_cost", name="Current month fixed cost", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/current-month/total_cost", name="Current month total cost", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/current-year/electricity1", name="Current year low tariff usage", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + 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, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + 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, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + 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, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + 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, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + 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, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), DSMRReaderSensorEntityDescription( key="dsmr/current-year/electricity1_cost", name="Current year low tariff cost", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/current-year/electricity2_cost", name="Current year high tariff cost", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/current-year/electricity_cost_merged", name="Current year power total cost", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/current-year/gas", name="Current year gas usage", icon="mdi:counter", - unit_of_measurement=VOLUME_CUBIC_METERS, + native_unit_of_measurement=VOLUME_CUBIC_METERS, ), DSMRReaderSensorEntityDescription( key="dsmr/current-year/gas_cost", name="Current year gas cost", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/current-year/fixed_cost", name="Current year fixed cost", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), DSMRReaderSensorEntityDescription( key="dsmr/current-year/total_cost", name="Current year total cost", icon="mdi:currency-eur", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), ) diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py index 39356db46b5..84947ec41f1 100644 --- a/homeassistant/components/dsmr_reader/sensor.py +++ b/homeassistant/components/dsmr_reader/sensor.py @@ -33,9 +33,9 @@ class DSMRSensor(SensorEntity): def message_received(message): """Handle new MQTT messages.""" if self.entity_description.state is not None: - self._attr_state = self.entity_description.state(message.payload) + self._attr_native_value = self.entity_description.state(message.payload) else: - self._attr_state = message.payload + self._attr_native_value = message.payload self.async_write_ha_state() diff --git a/homeassistant/components/dte_energy_bridge/sensor.py b/homeassistant/components/dte_energy_bridge/sensor.py index 4e095955818..5b08e8e142c 100644 --- a/homeassistant/components/dte_energy_bridge/sensor.py +++ b/homeassistant/components/dte_energy_bridge/sensor.py @@ -66,12 +66,12 @@ class DteEnergyBridgeSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/dublin_bus_transport/sensor.py b/homeassistant/components/dublin_bus_transport/sensor.py index dbe1d10b553..b7daf661e63 100644 --- a/homeassistant/components/dublin_bus_transport/sensor.py +++ b/homeassistant/components/dublin_bus_transport/sensor.py @@ -82,7 +82,7 @@ class DublinPublicTransportSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -105,7 +105,7 @@ class DublinPublicTransportSensor(SensorEntity): } @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return TIME_MINUTES diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 428ed3ab427..2668e573b7c 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -112,7 +112,7 @@ class DwdWeatherWarningsSensor(SensorEntity): self._attr_name = f"{name} {description.name}" @property - def state(self): + def native_value(self): """Return the state of the device.""" if self.entity_description.key == CURRENT_WARNING_SENSOR: return self._api.api.current_warning_level diff --git a/homeassistant/components/dweet/sensor.py b/homeassistant/components/dweet/sensor.py index f1243cd5407..3d980b34d00 100644 --- a/homeassistant/components/dweet/sensor.py +++ b/homeassistant/components/dweet/sensor.py @@ -73,12 +73,12 @@ class DweetSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the state.""" return self._state diff --git a/homeassistant/components/dyson/sensor.py b/homeassistant/components/dyson/sensor.py index cff4b8f5501..be83a7e4373 100644 --- a/homeassistant/components/dyson/sensor.py +++ b/homeassistant/components/dyson/sensor.py @@ -129,7 +129,7 @@ class DysonSensor(DysonEntity, SensorEntity): return f"{self._device.serial}-{self._sensor_type}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -152,7 +152,7 @@ class DysonFilterLifeSensor(DysonSensor): super().__init__(device, "filter_life") @property - def state(self): + def native_value(self): """Return filter life in hours.""" return int(self._device.state.filter_life) @@ -165,7 +165,7 @@ class DysonCarbonFilterLifeSensor(DysonSensor): super().__init__(device, "carbon_filter_state") @property - def state(self): + def native_value(self): """Return filter life remaining in percent.""" return int(self._device.state.carbon_filter_state) @@ -178,7 +178,7 @@ class DysonHepaFilterLifeSensor(DysonSensor): super().__init__(device, f"{filter_type}_filter_state") @property - def state(self): + def native_value(self): """Return filter life remaining in percent.""" return int(self._device.state.hepa_filter_state) @@ -191,7 +191,7 @@ class DysonDustSensor(DysonSensor): super().__init__(device, "dust") @property - def state(self): + def native_value(self): """Return Dust value.""" return self._device.environmental_state.dust @@ -204,7 +204,7 @@ class DysonHumiditySensor(DysonSensor): super().__init__(device, "humidity") @property - def state(self): + def native_value(self): """Return Humidity value.""" if self._device.environmental_state.humidity == 0: return STATE_OFF @@ -220,7 +220,7 @@ class DysonTemperatureSensor(DysonSensor): self._unit = unit @property - def state(self): + def native_value(self): """Return Temperature value.""" temperature_kelvin = self._device.environmental_state.temperature if temperature_kelvin == 0: @@ -230,7 +230,7 @@ class DysonTemperatureSensor(DysonSensor): return float(f"{(temperature_kelvin * 9 / 5 - 459.67):.1f}") @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit @@ -243,6 +243,6 @@ class DysonAirQualitySensor(DysonSensor): super().__init__(device, "air_quality") @property - def state(self): + def native_value(self): """Return Air Quality value.""" return int(self._device.environmental_state.volatil_organic_compounds) diff --git a/homeassistant/components/eafm/sensor.py b/homeassistant/components/eafm/sensor.py index b3d726f9cd3..bc2158e4db8 100644 --- a/homeassistant/components/eafm/sensor.py +++ b/homeassistant/components/eafm/sensor.py @@ -149,7 +149,7 @@ class Measurement(CoordinatorEntity, SensorEntity): return True @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return units for the sensor.""" measure = self.coordinator.data["measures"][self.key] if "unit" not in measure: @@ -162,6 +162,6 @@ class Measurement(CoordinatorEntity, SensorEntity): return {ATTR_ATTRIBUTION: self.attribution} @property - def state(self): + def native_value(self): """Return the current sensor value.""" return self.coordinator.data["measures"][self.key]["latestReading"]["value"] diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index e98dea45929..3c43dd36130 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -45,79 +45,79 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="usage", name="Usage", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:percent", ), SensorEntityDescription( key="balance", name="Balance", - unit_of_measurement=PRICE, + native_unit_of_measurement=PRICE, icon="mdi:cash-usd", ), SensorEntityDescription( key="limit", name="Data limit", - unit_of_measurement=DATA_GIGABITS, + native_unit_of_measurement=DATA_GIGABITS, icon="mdi:download", ), SensorEntityDescription( key="days_left", name="Days left", - unit_of_measurement=TIME_DAYS, + native_unit_of_measurement=TIME_DAYS, icon="mdi:calendar-today", ), SensorEntityDescription( key="before_offpeak_download", name="Download before offpeak", - unit_of_measurement=DATA_GIGABITS, + native_unit_of_measurement=DATA_GIGABITS, icon="mdi:download", ), SensorEntityDescription( key="before_offpeak_upload", name="Upload before offpeak", - unit_of_measurement=DATA_GIGABITS, + native_unit_of_measurement=DATA_GIGABITS, icon="mdi:upload", ), SensorEntityDescription( key="before_offpeak_total", name="Total before offpeak", - unit_of_measurement=DATA_GIGABITS, + native_unit_of_measurement=DATA_GIGABITS, icon="mdi:download", ), SensorEntityDescription( key="offpeak_download", name="Offpeak download", - unit_of_measurement=DATA_GIGABITS, + native_unit_of_measurement=DATA_GIGABITS, icon="mdi:download", ), SensorEntityDescription( key="offpeak_upload", name="Offpeak Upload", - unit_of_measurement=DATA_GIGABITS, + native_unit_of_measurement=DATA_GIGABITS, icon="mdi:upload", ), SensorEntityDescription( key="offpeak_total", name="Offpeak Total", - unit_of_measurement=DATA_GIGABITS, + native_unit_of_measurement=DATA_GIGABITS, icon="mdi:download", ), SensorEntityDescription( key="download", name="Download", - unit_of_measurement=DATA_GIGABITS, + native_unit_of_measurement=DATA_GIGABITS, icon="mdi:download", ), SensorEntityDescription( key="upload", name="Upload", - unit_of_measurement=DATA_GIGABITS, + native_unit_of_measurement=DATA_GIGABITS, icon="mdi:upload", ), SensorEntityDescription( key="total", name="Total", - unit_of_measurement=DATA_GIGABITS, + native_unit_of_measurement=DATA_GIGABITS, icon="mdi:download", ), ) @@ -179,7 +179,7 @@ class EBoxSensor(SensorEntity): """Get the latest data from EBox and update the state.""" await self.ebox_data.async_update() if self.entity_description.key in self.ebox_data.data: - self._attr_state = round( + self._attr_native_value = round( self.ebox_data.data[self.entity_description.key], 2 ) diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py index abd9620130d..dcfd4ec7eef 100644 --- a/homeassistant/components/ebusd/sensor.py +++ b/homeassistant/components/ebusd/sensor.py @@ -56,7 +56,7 @@ class EbusdSensor(SensorEntity): return f"{self._client_name} {self._name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -94,7 +94,7 @@ class EbusdSensor(SensorEntity): return self._icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement diff --git a/homeassistant/components/ecoal_boiler/sensor.py b/homeassistant/components/ecoal_boiler/sensor.py index 9a2fbdd9b87..d9689631280 100644 --- a/homeassistant/components/ecoal_boiler/sensor.py +++ b/homeassistant/components/ecoal_boiler/sensor.py @@ -33,7 +33,7 @@ class EcoalTempSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -43,7 +43,7 @@ class EcoalTempSensor(SensorEntity): return DEVICE_CLASS_TEMPERATURE @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return TEMP_CELSIUS diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 97f9fe6eae0..eb72f667b5f 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -107,7 +107,7 @@ class EcobeeSensor(SensorEntity): return None @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._state in ( ECOBEE_STATE_CALIBRATING, @@ -122,7 +122,7 @@ class EcobeeSensor(SensorEntity): return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return self._unit_of_measurement diff --git a/homeassistant/components/econet/sensor.py b/homeassistant/components/econet/sensor.py index 0dfe8df7fb3..bbcf54003e8 100644 --- a/homeassistant/components/econet/sensor.py +++ b/homeassistant/components/econet/sensor.py @@ -82,7 +82,7 @@ class EcoNetSensor(EcoNetEntity, SensorEntity): self._device_name = device_name @property - def state(self): + def native_value(self): """Return sensors state.""" value = getattr(self._econet, SENSOR_NAMES_TO_ATTRIBUTES[self._device_name]) if isinstance(value, float): @@ -90,7 +90,7 @@ class EcoNetSensor(EcoNetEntity, SensorEntity): return value @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" unit_of_measurement = SENSOR_NAMES_TO_UNIT_OF_MEASUREMENT[self._device_name] if self._device_name == POWER_USAGE_TODAY: diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index 9adb7665753..1eee0b47272 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -114,7 +114,7 @@ class EddystoneTemp(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the device.""" return self.temperature @@ -124,7 +124,7 @@ class EddystoneTemp(SensorEntity): return DEVICE_CLASS_TEMPERATURE @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return TEMP_CELSIUS diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index a00f77efa0b..407f5902198 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -301,7 +301,7 @@ class EDL21Entity(SensorEntity): return self._name @property - def state(self) -> str: + def native_value(self) -> str: """Return the value of the last received telegram.""" return self._telegram.get("value") @@ -315,7 +315,7 @@ class EDL21Entity(SensorEntity): } @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._telegram.get("unit") diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index 6e2ac1c01c7..391aca7b4af 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -120,12 +120,12 @@ class EfergySensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index 01413ceaec0..df0d7882491 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -101,12 +101,12 @@ class EightHeatSensor(EightSleepHeatEntity, SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return PERCENTAGE @@ -157,12 +157,12 @@ class EightUserSensor(EightSleepUserEntity, SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" if ( "current_sleep" in self._sensor @@ -316,7 +316,7 @@ class EightRoomSensor(EightSleepUserEntity, SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -333,7 +333,7 @@ class EightRoomSensor(EightSleepUserEntity, SensorEntity): self._state = None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" if self._units == "si": return TEMP_CELSIUS diff --git a/homeassistant/components/eliqonline/sensor.py b/homeassistant/components/eliqonline/sensor.py index 253913b3779..ecd6e4ad4bb 100644 --- a/homeassistant/components/eliqonline/sensor.py +++ b/homeassistant/components/eliqonline/sensor.py @@ -78,12 +78,12 @@ class EliqSensor(SensorEntity): return ICON @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return UNIT_OF_MEASUREMENT @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index 8f26af545b7..30fe87103c7 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -77,7 +77,7 @@ class ElkSensor(ElkAttachedEntity, SensorEntity): self._state = None @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -127,7 +127,7 @@ class ElkKeypad(ElkSensor): return self._temperature_unit @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._temperature_unit @@ -250,7 +250,7 @@ class ElkZone(ElkSensor): return None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" if self._element.definition == ZoneType.TEMPERATURE.value: return self._temperature_unit diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index bfc86db387e..5180275b528 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -162,12 +162,12 @@ class EmonCmsSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/emonitor/sensor.py b/homeassistant/components/emonitor/sensor.py index 1dca3f2d89d..1d699b42473 100644 --- a/homeassistant/components/emonitor/sensor.py +++ b/homeassistant/components/emonitor/sensor.py @@ -38,7 +38,7 @@ class EmonitorPowerSensor(CoordinatorEntity, SensorEntity): """Representation of an Emonitor power sensor entity.""" _attr_device_class = DEVICE_CLASS_POWER - _attr_unit_of_measurement = POWER_WATT + _attr_native_unit_of_measurement = POWER_WATT def __init__(self, coordinator: DataUpdateCoordinator, channel_number: int) -> None: """Initialize the channel sensor.""" @@ -73,7 +73,7 @@ class EmonitorPowerSensor(CoordinatorEntity, SensorEntity): return attr_val @property - def state(self) -> StateType: + def native_value(self) -> StateType: """State of the sensor.""" return self._paired_attr("inst_power") diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index e974035cbd6..ccf1a0d7b34 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -164,7 +164,7 @@ class EnergyCostSensor(SensorEntity): def _reset(self, energy_state: State) -> None: """Reset the cost sensor.""" - self._attr_state = 0.0 + self._attr_native_value = 0.0 self._cur_value = 0.0 self._attr_last_reset = dt_util.utcnow() self._last_energy_sensor_state = energy_state @@ -231,7 +231,7 @@ class EnergyCostSensor(SensorEntity): # Update with newly incurred cost old_energy_value = float(self._last_energy_sensor_state.state) self._cur_value += (energy - old_energy_value) * energy_price - self._attr_state = round(self._cur_value, 2) + self._attr_native_value = round(self._cur_value, 2) self._last_energy_sensor_state = energy_state @@ -281,6 +281,6 @@ class EnergyCostSensor(SensorEntity): self._flow = flow @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the units of measurement.""" return self.hass.config.currency diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index 1814efb9c87..ef7fe242092 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -130,12 +130,12 @@ class EnOceanSensor(EnOceanEntity, RestoreEntity, SensorEntity): return self._device_class @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index 9f87a821787..1d0dfba8990 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -20,27 +20,27 @@ SENSORS = ( SensorEntityDescription( key="production", name="Current Power Production", - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="daily_production", name="Today's Energy Production", - unit_of_measurement=ENERGY_WATT_HOUR, + native_unit_of_measurement=ENERGY_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, device_class=DEVICE_CLASS_ENERGY, ), SensorEntityDescription( key="seven_days_production", name="Last Seven Days Energy Production", - unit_of_measurement=ENERGY_WATT_HOUR, + native_unit_of_measurement=ENERGY_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, device_class=DEVICE_CLASS_ENERGY, ), SensorEntityDescription( key="lifetime_production", name="Lifetime Energy Production", - unit_of_measurement=ENERGY_WATT_HOUR, + native_unit_of_measurement=ENERGY_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, device_class=DEVICE_CLASS_ENERGY, last_reset=dt.utc_from_timestamp(0), @@ -48,27 +48,27 @@ SENSORS = ( SensorEntityDescription( key="consumption", name="Current Power Consumption", - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="daily_consumption", name="Today's Energy Consumption", - unit_of_measurement=ENERGY_WATT_HOUR, + native_unit_of_measurement=ENERGY_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, device_class=DEVICE_CLASS_ENERGY, ), SensorEntityDescription( key="seven_days_consumption", name="Last Seven Days Energy Consumption", - unit_of_measurement=ENERGY_WATT_HOUR, + native_unit_of_measurement=ENERGY_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, device_class=DEVICE_CLASS_ENERGY, ), SensorEntityDescription( key="lifetime_consumption", name="Lifetime Energy Consumption", - unit_of_measurement=ENERGY_WATT_HOUR, + native_unit_of_measurement=ENERGY_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, device_class=DEVICE_CLASS_ENERGY, last_reset=dt.utc_from_timestamp(0), @@ -76,7 +76,7 @@ SENSORS = ( SensorEntityDescription( key="inverters", name="Inverter", - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, state_class=STATE_CLASS_MEASUREMENT, ), ) diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 3af5cd1ec0c..9bf4073847e 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -132,7 +132,7 @@ class Envoy(CoordinatorEntity, SensorEntity): return f"{self._device_serial_number}_{self.entity_description.key}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.entity_description.key != "inverters": value = self.coordinator.data.get(self.entity_description.key) diff --git a/homeassistant/components/entur_public_transport/sensor.py b/homeassistant/components/entur_public_transport/sensor.py index 0852f95bd99..cad8a49884f 100644 --- a/homeassistant/components/entur_public_transport/sensor.py +++ b/homeassistant/components/entur_public_transport/sensor.py @@ -168,7 +168,7 @@ class EnturPublicTransportSensor(SensorEntity): return self._name @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" return self._state @@ -180,7 +180,7 @@ class EnturPublicTransportSensor(SensorEntity): return self._attributes @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return TIME_MINUTES diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 2c16eca9ea1..3690703d8d2 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -91,7 +91,7 @@ class ECSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -101,7 +101,7 @@ class ECSensor(SensorEntity): return self._attr @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the units of measurement.""" return self._unit diff --git a/homeassistant/components/envirophat/sensor.py b/homeassistant/components/envirophat/sensor.py index 9bca552326a..a41b1678faa 100644 --- a/homeassistant/components/envirophat/sensor.py +++ b/homeassistant/components/envirophat/sensor.py @@ -88,7 +88,7 @@ class EnvirophatSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -103,7 +103,7 @@ class EnvirophatSensor(SensorEntity): return SENSOR_TYPES[self.type][2] @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/envisalink/sensor.py b/homeassistant/components/envisalink/sensor.py index 6fd7f32c6fe..88aa7fa988c 100644 --- a/homeassistant/components/envisalink/sensor.py +++ b/homeassistant/components/envisalink/sensor.py @@ -61,7 +61,7 @@ class EnvisalinkSensor(EnvisalinkDevice, SensorEntity): return self._icon @property - def state(self): + def native_value(self): """Return the overall state.""" return self._info["status"]["alpha"] diff --git a/homeassistant/components/epsonworkforce/sensor.py b/homeassistant/components/epsonworkforce/sensor.py index 2f483b9fcbf..285f2fc83e7 100644 --- a/homeassistant/components/epsonworkforce/sensor.py +++ b/homeassistant/components/epsonworkforce/sensor.py @@ -20,37 +20,37 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="black", name="Ink level Black", icon="mdi:water", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( key="photoblack", name="Ink level Photoblack", icon="mdi:water", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( key="magenta", name="Ink level Magenta", icon="mdi:water", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( key="cyan", name="Ink level Cyan", icon="mdi:water", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( key="yellow", name="Ink level Yellow", icon="mdi:water", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( key="clean", name="Cleaning level", icon="mdi:water", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, ), ) MONITORED_CONDITIONS: list[str] = [desc.key for desc in SENSOR_TYPES] @@ -92,7 +92,7 @@ class EpsonPrinterCartridge(SensorEntity): self.entity_description = description @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._api.getSensorValue(self.entity_description.key) diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 6a2b51498f0..3e8fbc19a4d 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -161,7 +161,7 @@ class EsphomeSensor( return self._static_info.force_update @esphome_state_property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the entity.""" if math.isnan(self._state.state): return None @@ -172,7 +172,7 @@ class EsphomeSensor( return f"{self._state.state:.{self._static_info.accuracy_decimals}f}" @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" if not self._static_info.unit_of_measurement: return None @@ -202,7 +202,7 @@ class EsphomeTextSensor(EsphomeEntity[TextSensorInfo, TextSensorState], SensorEn return self._static_info.icon @esphome_state_property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the entity.""" if self._state.missing_state: return None diff --git a/homeassistant/components/essent/sensor.py b/homeassistant/components/essent/sensor.py index f0dc70d7be4..42a4c1c399b 100644 --- a/homeassistant/components/essent/sensor.py +++ b/homeassistant/components/essent/sensor.py @@ -104,12 +104,12 @@ class EssentMeter(SensorEntity): return f"Essent {self._type} ({self._tariff})" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" if self._unit.lower() == "kwh": return ENERGY_KILO_WATT_HOUR diff --git a/homeassistant/components/etherscan/sensor.py b/homeassistant/components/etherscan/sensor.py index 1b10cc39fe1..b1ec3cddb0c 100644 --- a/homeassistant/components/etherscan/sensor.py +++ b/homeassistant/components/etherscan/sensor.py @@ -59,12 +59,12 @@ class EtherscanSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return self._unit_of_measurement diff --git a/homeassistant/components/ezviz/sensor.py b/homeassistant/components/ezviz/sensor.py index 4e81ef6a6a7..42283b52d35 100644 --- a/homeassistant/components/ezviz/sensor.py +++ b/homeassistant/components/ezviz/sensor.py @@ -66,7 +66,7 @@ class EzvizSensor(CoordinatorEntity, Entity): return self._name @property - def state(self) -> int | str: + def native_value(self) -> int | str: """Return the state of the sensor.""" return self.coordinator.data[self._idx][self._name] From 94a264afaf5a989a88b66172b7c6dd44d8ff6e16 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Aug 2021 18:57:50 +0200 Subject: [PATCH 162/355] Move temperature conversions to entity base class (7/8) (#54482) --- homeassistant/components/starline/sensor.py | 4 +- .../components/starlingbank/sensor.py | 4 +- homeassistant/components/startca/sensor.py | 4 +- homeassistant/components/statistics/sensor.py | 4 +- .../components/steam_online/sensor.py | 2 +- .../components/streamlabswater/sensor.py | 8 +-- homeassistant/components/subaru/sensor.py | 4 +- homeassistant/components/suez_water/sensor.py | 4 +- .../components/supervisord/sensor.py | 2 +- .../components/surepetcare/sensor.py | 6 +-- .../swiss_hydrological_data/sensor.py | 4 +- .../swiss_public_transport/sensor.py | 2 +- .../components/switcher_kis/sensor.py | 4 +- homeassistant/components/syncthing/sensor.py | 2 +- homeassistant/components/syncthru/sensor.py | 12 ++--- .../components/synology_dsm/sensor.py | 8 +-- .../components/system_bridge/sensor.py | 28 +++++----- .../components/systemmonitor/sensor.py | 4 +- homeassistant/components/tado/sensor.py | 8 +-- homeassistant/components/tahoma/sensor.py | 4 +- .../components/tank_utility/sensor.py | 4 +- .../components/tankerkoenig/sensor.py | 4 +- homeassistant/components/tasmota/sensor.py | 4 +- homeassistant/components/tautulli/sensor.py | 4 +- homeassistant/components/tcp/sensor.py | 4 +- homeassistant/components/ted5000/sensor.py | 4 +- .../components/tellduslive/sensor.py | 4 +- homeassistant/components/tellstick/sensor.py | 4 +- homeassistant/components/temper/sensor.py | 4 +- homeassistant/components/template/sensor.py | 8 +-- homeassistant/components/tesla/sensor.py | 4 +- .../components/thermoworks_smoke/sensor.py | 4 +- .../components/thethingsnetwork/sensor.py | 4 +- .../components/thinkingcleaner/sensor.py | 4 +- homeassistant/components/tibber/sensor.py | 54 +++++++++---------- homeassistant/components/time_date/sensor.py | 2 +- homeassistant/components/tmb/sensor.py | 4 +- homeassistant/components/tof/sensor.py | 4 +- homeassistant/components/toon/sensor.py | 4 +- homeassistant/components/torque/sensor.py | 4 +- homeassistant/components/tradfri/sensor.py | 4 +- .../components/trafikverket_train/sensor.py | 2 +- .../trafikverket_weatherstation/sensor.py | 4 +- .../components/transmission/sensor.py | 6 +-- .../components/transport_nsw/sensor.py | 4 +- homeassistant/components/travisci/sensor.py | 4 +- .../components/twentemilieu/sensor.py | 2 +- homeassistant/components/twitch/sensor.py | 2 +- .../components/uk_transport/sensor.py | 4 +- homeassistant/components/unifi/sensor.py | 8 +-- homeassistant/components/upnp/sensor.py | 8 +-- homeassistant/components/uptime/sensor.py | 2 +- homeassistant/components/uscis/sensor.py | 2 +- .../components/utility_meter/sensor.py | 4 +- 54 files changed, 153 insertions(+), 153 deletions(-) diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index e7996befad3..92c6acbab0b 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -69,7 +69,7 @@ class StarlineSensor(StarlineEntity, SensorEntity): return self._icon @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._key == "battery": return self._device.battery_level @@ -90,7 +90,7 @@ class StarlineSensor(StarlineEntity, SensorEntity): return None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Get the unit of measurement.""" if self._key == "balance": return self._device.balance.get("currency") or "₽" diff --git a/homeassistant/components/starlingbank/sensor.py b/homeassistant/components/starlingbank/sensor.py index 77f5ab307cb..ae1ac2d4987 100644 --- a/homeassistant/components/starlingbank/sensor.py +++ b/homeassistant/components/starlingbank/sensor.py @@ -79,12 +79,12 @@ class StarlingBalanceSensor(SensorEntity): ) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._starling_account.currency diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index 661e00ed494..d4124ec3d7c 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -93,12 +93,12 @@ class StartcaSensor(SensorEntity): return f"{self.client_name} {self._name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index de32603c207..ea90346fe7c 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -203,12 +203,12 @@ class StatisticsSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self.mean if not self.is_binary else self.count @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement if not self.is_binary else None diff --git a/homeassistant/components/steam_online/sensor.py b/homeassistant/components/steam_online/sensor.py index 45ae1a6c70a..18f7c6cc447 100644 --- a/homeassistant/components/steam_online/sensor.py +++ b/homeassistant/components/steam_online/sensor.py @@ -99,7 +99,7 @@ class SteamSensor(SensorEntity): return f"sensor.steam_{self._account}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/streamlabswater/sensor.py b/homeassistant/components/streamlabswater/sensor.py index ba722d0a4f2..3af87b8a3f8 100644 --- a/homeassistant/components/streamlabswater/sensor.py +++ b/homeassistant/components/streamlabswater/sensor.py @@ -87,12 +87,12 @@ class StreamLabsDailyUsage(SensorEntity): return WATER_ICON @property - def state(self): + def native_value(self): """Return the current daily usage.""" return self._streamlabs_usage_data.get_daily_usage() @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return gallons as the unit measurement for water.""" return VOLUME_GALLONS @@ -110,7 +110,7 @@ class StreamLabsMonthlyUsage(StreamLabsDailyUsage): return f"{self._location_name} {NAME_MONTHLY_USAGE}" @property - def state(self): + def native_value(self): """Return the current monthly usage.""" return self._streamlabs_usage_data.get_monthly_usage() @@ -124,6 +124,6 @@ class StreamLabsYearlyUsage(StreamLabsDailyUsage): return f"{self._location_name} {NAME_YEARLY_USAGE}" @property - def state(self): + def native_value(self): """Return the current yearly usage.""" return self._streamlabs_usage_data.get_yearly_usage() diff --git a/homeassistant/components/subaru/sensor.py b/homeassistant/components/subaru/sensor.py index ff1d8b715d7..7aeab66b929 100644 --- a/homeassistant/components/subaru/sensor.py +++ b/homeassistant/components/subaru/sensor.py @@ -203,7 +203,7 @@ class SubaruSensor(SubaruEntity, SensorEntity): return None @property - def state(self): + def native_value(self): """Return the state of the sensor.""" self.current_value = self.get_current_value() @@ -238,7 +238,7 @@ class SubaruSensor(SubaruEntity, SensorEntity): return self.current_value @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit_of_measurement of the device.""" if self.api_unit in TEMPERATURE_UNITS: return self.hass.config.units.temperature_unit diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py index 7170e0b8a67..c9c125e8e7e 100644 --- a/homeassistant/components/suez_water/sensor.py +++ b/homeassistant/components/suez_water/sensor.py @@ -62,12 +62,12 @@ class SuezSensor(SensorEntity): return COMPONENT_NAME @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return VOLUME_LITERS diff --git a/homeassistant/components/supervisord/sensor.py b/homeassistant/components/supervisord/sensor.py index f701df2d6c3..5db8680f1c9 100644 --- a/homeassistant/components/supervisord/sensor.py +++ b/homeassistant/components/supervisord/sensor.py @@ -50,7 +50,7 @@ class SupervisorProcessSensor(SensorEntity): return self._info.get("name") @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._info.get("statename") diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py index fbc8222f292..922bfa84515 100644 --- a/homeassistant/components/surepetcare/sensor.py +++ b/homeassistant/components/surepetcare/sensor.py @@ -60,7 +60,7 @@ class SureBattery(SensorEntity): self._attr_device_class = DEVICE_CLASS_BATTERY self._attr_name = f"{surepy_entity.type.name.capitalize()} {surepy_entity.name.capitalize()} Battery Level" - self._attr_unit_of_measurement = PERCENTAGE + self._attr_native_unit_of_measurement = PERCENTAGE self._attr_unique_id = ( f"{surepy_entity.household_id}-{surepy_entity.id}-battery" ) @@ -75,11 +75,11 @@ class SureBattery(SensorEntity): try: per_battery_voltage = state["battery"] / 4 voltage_diff = per_battery_voltage - SURE_BATT_VOLTAGE_LOW - self._attr_state = min( + self._attr_native_value = min( int(voltage_diff / SURE_BATT_VOLTAGE_DIFF * 100), 100 ) except (KeyError, TypeError): - self._attr_state = None + self._attr_native_value = None if state: voltage_per_battery = float(state["battery"]) / 4 diff --git a/homeassistant/components/swiss_hydrological_data/sensor.py b/homeassistant/components/swiss_hydrological_data/sensor.py index 1d77410f031..3daa7161869 100644 --- a/homeassistant/components/swiss_hydrological_data/sensor.py +++ b/homeassistant/components/swiss_hydrological_data/sensor.py @@ -94,14 +94,14 @@ class SwissHydrologicalDataSensor(SensorEntity): return f"{self._station}_{self._condition}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" if self._state is not None: return self.hydro_data.data["parameters"][self._condition]["unit"] return None @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if isinstance(self._state, (int, float)): return round(self._state, 2) diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py index a971524c22b..0f0ac28d530 100644 --- a/homeassistant/components/swiss_public_transport/sensor.py +++ b/homeassistant/components/swiss_public_transport/sensor.py @@ -84,7 +84,7 @@ class SwissPublicTransportSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return ( self._opendata.connections[0]["departure"] diff --git a/homeassistant/components/switcher_kis/sensor.py b/homeassistant/components/switcher_kis/sensor.py index 705c6f0a2b6..e070bd52d0d 100644 --- a/homeassistant/components/switcher_kis/sensor.py +++ b/homeassistant/components/switcher_kis/sensor.py @@ -110,7 +110,7 @@ class SwitcherSensorEntity(CoordinatorEntity, SensorEntity): # Entity class attributes self._attr_name = f"{wrapper.name} {description.name}" self._attr_icon = description.icon - self._attr_unit_of_measurement = description.unit + self._attr_native_unit_of_measurement = description.unit self._attr_device_class = description.device_class self._attr_entity_registry_enabled_default = description.default_enabled @@ -122,6 +122,6 @@ class SwitcherSensorEntity(CoordinatorEntity, SensorEntity): } @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return value of sensor.""" return getattr(self.wrapper.data, self.attribute) # type: ignore[no-any-return] diff --git a/homeassistant/components/syncthing/sensor.py b/homeassistant/components/syncthing/sensor.py index 5e8ea2f88c2..924f8aaf669 100644 --- a/homeassistant/components/syncthing/sensor.py +++ b/homeassistant/components/syncthing/sensor.py @@ -105,7 +105,7 @@ class FolderSensor(SensorEntity): return f"{self._short_server_id}-{self._folder_id}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state["state"] diff --git a/homeassistant/components/syncthru/sensor.py b/homeassistant/components/syncthru/sensor.py index 2b559e0a15f..cc832f77f0a 100644 --- a/homeassistant/components/syncthru/sensor.py +++ b/homeassistant/components/syncthru/sensor.py @@ -124,7 +124,7 @@ class SyncThruSensor(CoordinatorEntity, SensorEntity): return self._icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measuremnt.""" return self._unit_of_measurement @@ -148,7 +148,7 @@ class SyncThruMainSensor(SyncThruSensor): self._id_suffix = "_main" @property - def state(self): + def native_value(self): """Set state to human readable version of syncthru status.""" return SYNCTHRU_STATE_HUMAN[self.syncthru.device_status()] @@ -182,7 +182,7 @@ class SyncThruTonerSensor(SyncThruSensor): return self.syncthru.toner_status().get(self._color, {}) @property - def state(self): + def native_value(self): """Show amount of remaining toner.""" return self.syncthru.toner_status().get(self._color, {}).get("remaining") @@ -204,7 +204,7 @@ class SyncThruDrumSensor(SyncThruSensor): return self.syncthru.drum_status().get(self._color, {}) @property - def state(self): + def native_value(self): """Show amount of remaining drum.""" return self.syncthru.drum_status().get(self._color, {}).get("remaining") @@ -225,7 +225,7 @@ class SyncThruInputTraySensor(SyncThruSensor): return self.syncthru.input_tray_status().get(self._number, {}) @property - def state(self): + def native_value(self): """Display ready unless there is some error, then display error.""" tray_state = ( self.syncthru.input_tray_status().get(self._number, {}).get("newError") @@ -251,7 +251,7 @@ class SyncThruOutputTraySensor(SyncThruSensor): return self.syncthru.output_tray_status().get(self._number, {}) @property - def state(self): + def native_value(self): """Display ready unless there is some error, then display error.""" tray_state = ( self.syncthru.output_tray_status().get(self._number, {}).get("status") diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 5942ce4a5b1..1ddc79c0afc 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -90,7 +90,7 @@ class SynoDSMSensor(SynologyDSMBaseEntity): """Mixin for sensor specific attributes.""" @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" if self.entity_type in TEMP_SENSORS_KEYS: return self.hass.config.units.temperature_unit @@ -101,7 +101,7 @@ class SynoDSMUtilSensor(SynoDSMSensor, SensorEntity): """Representation a Synology Utilisation sensor.""" @property - def state(self) -> Any | None: + def native_value(self) -> Any | None: """Return the state.""" attr = getattr(self._api.utilisation, self.entity_type) if callable(attr): @@ -133,7 +133,7 @@ class SynoDSMStorageSensor(SynologyDSMDeviceEntity, SynoDSMSensor, SensorEntity) """Representation a Synology Storage sensor.""" @property - def state(self) -> Any | None: + def native_value(self) -> Any | None: """Return the state.""" attr = getattr(self._api.storage, self.entity_type)(self._device_id) if attr is None: @@ -166,7 +166,7 @@ class SynoDSMInfoSensor(SynoDSMSensor, SensorEntity): self._last_boot: str | None = None @property - def state(self) -> Any | None: + def native_value(self) -> Any | None: """Return the state.""" attr = getattr(self._api.information, self.entity_type) if attr is None: diff --git a/homeassistant/components/system_bridge/sensor.py b/homeassistant/components/system_bridge/sensor.py index ed3c569f10f..acfcc54f05c 100644 --- a/homeassistant/components/system_bridge/sensor.py +++ b/homeassistant/components/system_bridge/sensor.py @@ -92,7 +92,7 @@ class SystemBridgeSensor(SystemBridgeDeviceEntity, SensorEntity): return self._device_class @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit this state is expressed in.""" return self._unit_of_measurement @@ -113,7 +113,7 @@ class SystemBridgeBatterySensor(SystemBridgeSensor): ) @property - def state(self) -> float: + def native_value(self) -> float: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return bridge.battery.percent @@ -135,7 +135,7 @@ class SystemBridgeBatteryTimeRemainingSensor(SystemBridgeSensor): ) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data if bridge.battery.timeRemaining is None: @@ -159,7 +159,7 @@ class SystemBridgeCpuSpeedSensor(SystemBridgeSensor): ) @property - def state(self) -> float: + def native_value(self) -> float: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return bridge.cpu.currentSpeed.avg @@ -181,7 +181,7 @@ class SystemBridgeCpuTemperatureSensor(SystemBridgeSensor): ) @property - def state(self) -> float: + def native_value(self) -> float: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return bridge.cpu.temperature.main @@ -203,7 +203,7 @@ class SystemBridgeCpuVoltageSensor(SystemBridgeSensor): ) @property - def state(self) -> float: + def native_value(self) -> float: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return bridge.cpu.cpu.voltage @@ -229,7 +229,7 @@ class SystemBridgeFilesystemSensor(SystemBridgeSensor): self._fs_key = key @property - def state(self) -> float: + def native_value(self) -> float: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return ( @@ -268,7 +268,7 @@ class SystemBridgeMemoryFreeSensor(SystemBridgeSensor): ) @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return ( @@ -294,7 +294,7 @@ class SystemBridgeMemoryUsedSensor(SystemBridgeSensor): ) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return ( @@ -320,7 +320,7 @@ class SystemBridgeMemoryUsedPercentageSensor(SystemBridgeSensor): ) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return ( @@ -346,7 +346,7 @@ class SystemBridgeKernelSensor(SystemBridgeSensor): ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return bridge.os.kernel @@ -368,7 +368,7 @@ class SystemBridgeOsSensor(SystemBridgeSensor): ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return f"{bridge.os.distro} {bridge.os.release}" @@ -390,7 +390,7 @@ class SystemBridgeProcessesLoadSensor(SystemBridgeSensor): ) @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return ( @@ -431,7 +431,7 @@ class SystemBridgeBiosVersionSensor(SystemBridgeSensor): ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" bridge: Bridge = self.coordinator.data return bridge.system.bios.version diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index bc3e922a923..687e9e8e521 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -331,12 +331,12 @@ class SystemMonitorSensor(SensorEntity): return self.sensor_type[SENSOR_TYPE_ICON] # type: ignore[no-any-return] @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the device.""" return self.data.state @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" return self.sensor_type[SENSOR_TYPE_UOM] # type: ignore[no-any-return] diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index e1219b5620b..537e094bfd2 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -127,7 +127,7 @@ class TadoHomeSensor(TadoHomeEntity, SensorEntity): return f"{self._tado.home_name} {self.home_variable}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -137,7 +137,7 @@ class TadoHomeSensor(TadoHomeEntity, SensorEntity): return self._state_attributes @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" if self.home_variable in ["temperature", "outdoor temperature"]: return TEMP_CELSIUS @@ -232,7 +232,7 @@ class TadoZoneSensor(TadoZoneEntity, SensorEntity): return f"{self.zone_name} {self.zone_variable}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -242,7 +242,7 @@ class TadoZoneSensor(TadoZoneEntity, SensorEntity): return self._state_attributes @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" if self.zone_variable == "temperature": return self.hass.config.units.temperature_unit diff --git a/homeassistant/components/tahoma/sensor.py b/homeassistant/components/tahoma/sensor.py index 47e6d300414..35a51b03805 100644 --- a/homeassistant/components/tahoma/sensor.py +++ b/homeassistant/components/tahoma/sensor.py @@ -35,12 +35,12 @@ class TahomaSensor(TahomaDevice, SensorEntity): super().__init__(tahoma_device, controller) @property - def state(self): + def native_value(self): """Return the name of the sensor.""" return self.current_value @property - def unit_of_measurement(self): + 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 diff --git a/homeassistant/components/tank_utility/sensor.py b/homeassistant/components/tank_utility/sensor.py index 379819cf65e..93794ce0c50 100644 --- a/homeassistant/components/tank_utility/sensor.py +++ b/homeassistant/components/tank_utility/sensor.py @@ -81,7 +81,7 @@ class TankUtilitySensor(SensorEntity): return self._device @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state @@ -91,7 +91,7 @@ class TankUtilitySensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of the device.""" return self._unit_of_measurement diff --git a/homeassistant/components/tankerkoenig/sensor.py b/homeassistant/components/tankerkoenig/sensor.py index 5c1898e02a9..166e1da7060 100644 --- a/homeassistant/components/tankerkoenig/sensor.py +++ b/homeassistant/components/tankerkoenig/sensor.py @@ -110,12 +110,12 @@ class FuelPriceSensor(CoordinatorEntity, SensorEntity): return ICON @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return unit of measurement.""" return CURRENCY_EURO @property - def state(self): + def native_value(self): """Return the state of the device.""" # key Fuel_type is not available when the fuel station is closed, use "get" instead of "[]" to avoid exceptions return self.coordinator.data[self._station_id].get(self._fuel_type) diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index b756d656921..29144370ae7 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -258,7 +258,7 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): return class_or_icon.get(ICON) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the entity.""" if self._state_timestamp and self.device_class == DEVICE_CLASS_TIMESTAMP: return self._state_timestamp.isoformat() @@ -270,6 +270,6 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): return True @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit this state is expressed in.""" return SENSOR_UNIT_MAP.get(self._tasmota_entity.unit, self._tasmota_entity.unit) diff --git a/homeassistant/components/tautulli/sensor.py b/homeassistant/components/tautulli/sensor.py index c50efb00ed7..67df02cb15d 100644 --- a/homeassistant/components/tautulli/sensor.py +++ b/homeassistant/components/tautulli/sensor.py @@ -114,7 +114,7 @@ class TautulliSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self.sessions.get("stream_count") @@ -124,7 +124,7 @@ class TautulliSensor(SensorEntity): return "mdi:plex" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return "Watching" diff --git a/homeassistant/components/tcp/sensor.py b/homeassistant/components/tcp/sensor.py index d282974fd4c..4db511e1f57 100644 --- a/homeassistant/components/tcp/sensor.py +++ b/homeassistant/components/tcp/sensor.py @@ -31,11 +31,11 @@ class TcpSensor(TcpEntity, SensorEntity): """Implementation of a TCP socket based sensor.""" @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the device.""" return self._state @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity.""" return self._config[CONF_UNIT_OF_MEASUREMENT] diff --git a/homeassistant/components/ted5000/sensor.py b/homeassistant/components/ted5000/sensor.py index 6732014c747..a7162ee9c63 100644 --- a/homeassistant/components/ted5000/sensor.py +++ b/homeassistant/components/ted5000/sensor.py @@ -79,12 +79,12 @@ class Ted5000Sensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit @property - def state(self): + def native_value(self): """Return the state of the resources.""" with suppress(KeyError): return self._gateway.data[self._mtu][self._unit] diff --git a/homeassistant/components/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py index a86b487afd2..35fc6809523 100644 --- a/homeassistant/components/tellduslive/sensor.py +++ b/homeassistant/components/tellduslive/sensor.py @@ -111,7 +111,7 @@ class TelldusLiveSensor(TelldusLiveEntity, SensorEntity): return "{} {}".format(super().name, self.quantity_name or "").strip() @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if not self.available: return None @@ -129,7 +129,7 @@ class TelldusLiveSensor(TelldusLiveEntity, SensorEntity): return SENSOR_TYPES[self._type][0] if self._type in SENSOR_TYPES else None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return SENSOR_TYPES[self._type][1] if self._type in SENSOR_TYPES else None diff --git a/homeassistant/components/tellstick/sensor.py b/homeassistant/components/tellstick/sensor.py index 599c19388d6..74548f94d1b 100644 --- a/homeassistant/components/tellstick/sensor.py +++ b/homeassistant/components/tellstick/sensor.py @@ -154,12 +154,12 @@ class TellstickSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._value @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py index 7d447d3f9ea..ffb5660109c 100644 --- a/homeassistant/components/temper/sensor.py +++ b/homeassistant/components/temper/sensor.py @@ -77,12 +77,12 @@ class TemperSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the entity.""" return self.current_value @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self.temp_unit diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index a887890510a..d51b18e294b 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -255,7 +255,7 @@ class SensorTemplate(TemplateEntity, SensorEntity): except template.TemplateError: pass - self._attr_unit_of_measurement = unit_of_measurement + self._attr_native_unit_of_measurement = unit_of_measurement self._template = state_template self._attr_device_class = device_class self._attr_state_class = state_class @@ -264,7 +264,7 @@ class SensorTemplate(TemplateEntity, SensorEntity): async def async_added_to_hass(self): """Register callbacks.""" self.add_template_attribute( - "_attr_state", self._template, None, self._update_state + "_attr_native_value", self._template, None, self._update_state ) if self._friendly_name_template and not self._friendly_name_template.is_static: self.add_template_attribute("_attr_name", self._friendly_name_template) @@ -274,7 +274,7 @@ class SensorTemplate(TemplateEntity, SensorEntity): @callback def _update_state(self, result): super()._update_state(result) - self._attr_state = None if isinstance(result, TemplateError) else result + self._attr_native_value = None if isinstance(result, TemplateError) else result class TriggerSensorEntity(TriggerEntity, SensorEntity): @@ -284,7 +284,7 @@ class TriggerSensorEntity(TriggerEntity, SensorEntity): extra_template_keys = (CONF_STATE,) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return state of the sensor.""" return self._rendered.get(CONF_STATE) diff --git a/homeassistant/components/tesla/sensor.py b/homeassistant/components/tesla/sensor.py index ad585082b48..60e3e19047d 100644 --- a/homeassistant/components/tesla/sensor.py +++ b/homeassistant/components/tesla/sensor.py @@ -38,7 +38,7 @@ class TeslaSensor(TeslaDevice, SensorEntity): self._unique_id = f"{super().unique_id}_{self.type}" @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the state of the sensor.""" if self.tesla_device.type == "temperature sensor": if self.type == "outside": @@ -57,7 +57,7 @@ class TeslaSensor(TeslaDevice, SensorEntity): return self.tesla_device.get_value() @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit_of_measurement of the device.""" units = self.tesla_device.measurement if units == "F": diff --git a/homeassistant/components/thermoworks_smoke/sensor.py b/homeassistant/components/thermoworks_smoke/sensor.py index 4b14c9a9305..2e4ef6e56ec 100644 --- a/homeassistant/components/thermoworks_smoke/sensor.py +++ b/homeassistant/components/thermoworks_smoke/sensor.py @@ -120,7 +120,7 @@ class ThermoworksSmokeSensor(SensorEntity): return self._unique_id @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -130,7 +130,7 @@ class ThermoworksSmokeSensor(SensorEntity): return self._attributes @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this sensor.""" return self._unit_of_measurement diff --git a/homeassistant/components/thethingsnetwork/sensor.py b/homeassistant/components/thethingsnetwork/sensor.py index 2e139eae63d..089d1eda2ee 100644 --- a/homeassistant/components/thethingsnetwork/sensor.py +++ b/homeassistant/components/thethingsnetwork/sensor.py @@ -76,7 +76,7 @@ class TtnDataSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the entity.""" if self._ttn_data_storage.data is not None: try: @@ -86,7 +86,7 @@ class TtnDataSensor(SensorEntity): return None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/thinkingcleaner/sensor.py b/homeassistant/components/thinkingcleaner/sensor.py index fa1dfd5988c..e7530636169 100644 --- a/homeassistant/components/thinkingcleaner/sensor.py +++ b/homeassistant/components/thinkingcleaner/sensor.py @@ -95,12 +95,12 @@ class ThinkingCleanerSensor(SensorEntity): return SENSOR_TYPES[self.type][2] @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index b5012cdc41d..6674f6829f9 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -71,39 +71,39 @@ RT_SENSOR_MAP: dict[str, TibberSensorEntityDescription] = { key="averagePower", name="average power", device_class=DEVICE_CLASS_POWER, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, ), "power": TibberSensorEntityDescription( key="power", name="power", device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, ), "powerProduction": TibberSensorEntityDescription( key="powerProduction", name="power production", device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, ), "minPower": TibberSensorEntityDescription( key="minPower", name="min power", device_class=DEVICE_CLASS_POWER, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, ), "maxPower": TibberSensorEntityDescription( key="maxPower", name="max power", device_class=DEVICE_CLASS_POWER, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, ), "accumulatedConsumption": TibberSensorEntityDescription( key="accumulatedConsumption", name="accumulated consumption", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, reset_type=ResetType.DAILY, ), @@ -111,7 +111,7 @@ RT_SENSOR_MAP: dict[str, TibberSensorEntityDescription] = { key="accumulatedConsumptionLastHour", name="accumulated consumption current hour", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, reset_type=ResetType.HOURLY, ), @@ -119,7 +119,7 @@ RT_SENSOR_MAP: dict[str, TibberSensorEntityDescription] = { key="accumulatedProduction", name="accumulated production", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, reset_type=ResetType.DAILY, ), @@ -127,7 +127,7 @@ RT_SENSOR_MAP: dict[str, TibberSensorEntityDescription] = { key="accumulatedProductionLastHour", name="accumulated production current hour", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, reset_type=ResetType.HOURLY, ), @@ -135,63 +135,63 @@ RT_SENSOR_MAP: dict[str, TibberSensorEntityDescription] = { key="lastMeterConsumption", name="last meter consumption", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, ), "lastMeterProduction": TibberSensorEntityDescription( key="lastMeterProduction", name="last meter production", device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, ), "voltagePhase1": TibberSensorEntityDescription( key="voltagePhase1", name="voltage phase1", device_class=DEVICE_CLASS_VOLTAGE, - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), "voltagePhase2": TibberSensorEntityDescription( key="voltagePhase2", name="voltage phase2", device_class=DEVICE_CLASS_VOLTAGE, - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), "voltagePhase3": TibberSensorEntityDescription( key="voltagePhase3", name="voltage phase3", device_class=DEVICE_CLASS_VOLTAGE, - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), "currentL1": TibberSensorEntityDescription( key="currentL1", name="current L1", device_class=DEVICE_CLASS_CURRENT, - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), "currentL2": TibberSensorEntityDescription( key="currentL2", name="current L2", device_class=DEVICE_CLASS_CURRENT, - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), "currentL3": TibberSensorEntityDescription( key="currentL3", name="current L3", device_class=DEVICE_CLASS_CURRENT, - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), "signalStrength": TibberSensorEntityDescription( key="signalStrength", name="signal strength", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, - unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, state_class=STATE_CLASS_MEASUREMENT, ), "accumulatedReward": TibberSensorEntityDescription( @@ -212,7 +212,7 @@ RT_SENSOR_MAP: dict[str, TibberSensorEntityDescription] = { key="powerFactor", name="power factor", device_class=DEVICE_CLASS_POWER_FACTOR, - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), } @@ -350,13 +350,13 @@ class TibberSensorElPrice(TibberSensor): return res = self._tibber_home.current_price_data() - self._attr_state, price_level, self._last_updated = res + self._attr_native_value, price_level, self._last_updated = res self._attr_extra_state_attributes["price_level"] = price_level attrs = self._tibber_home.current_attributes() self._attr_extra_state_attributes.update(attrs) - self._attr_available = self._attr_state is not None - self._attr_unit_of_measurement = self._tibber_home.price_unit + self._attr_available = self._attr_native_value is not None + self._attr_native_unit_of_measurement = self._tibber_home.price_unit @Throttle(MIN_TIME_BETWEEN_UPDATES) async def _fetch_data(self): @@ -394,11 +394,11 @@ class TibberSensorRT(TibberSensor): self._device_name = f"{self._model} {self._home_name}" self._attr_name = f"{description.name} {self._home_name}" - self._attr_state = initial_state + self._attr_native_value = initial_state self._attr_unique_id = f"{self._tibber_home.home_id}_rt_{description.name}" if description.name in ("accumulated cost", "accumulated reward"): - self._attr_unit_of_measurement = tibber_home.currency + self._attr_native_unit_of_measurement = tibber_home.currency if description.reset_type == ResetType.NEVER: self._attr_last_reset = dt_util.utc_from_timestamp(0) elif description.reset_type == ResetType.DAILY: @@ -431,20 +431,20 @@ class TibberSensorRT(TibberSensor): def _set_state(self, state, timestamp): """Set sensor state.""" if ( - state < self._attr_state + state < self._attr_native_value and self.entity_description.reset_type == ResetType.DAILY ): self._attr_last_reset = dt_util.as_utc( timestamp.replace(hour=0, minute=0, second=0, microsecond=0) ) if ( - state < self._attr_state + state < self._attr_native_value and self.entity_description.reset_type == ResetType.HOURLY ): self._attr_last_reset = dt_util.as_utc( timestamp.replace(minute=0, second=0, microsecond=0) ) - self._attr_state = state + self._attr_native_value = state self.async_write_ha_state() diff --git a/homeassistant/components/time_date/sensor.py b/homeassistant/components/time_date/sensor.py index 08195e6dd3d..58582b3b139 100644 --- a/homeassistant/components/time_date/sensor.py +++ b/homeassistant/components/time_date/sensor.py @@ -65,7 +65,7 @@ class TimeDateSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/tmb/sensor.py b/homeassistant/components/tmb/sensor.py index 88471a86c27..d777fec38b6 100644 --- a/homeassistant/components/tmb/sensor.py +++ b/homeassistant/components/tmb/sensor.py @@ -85,7 +85,7 @@ class TMBSensor(SensorEntity): return ICON @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit @@ -95,7 +95,7 @@ class TMBSensor(SensorEntity): return f"{self._stop}_{self._line}" @property - def state(self): + def native_value(self): """Return the next departure time.""" return self._state diff --git a/homeassistant/components/tof/sensor.py b/homeassistant/components/tof/sensor.py index 45713dd8f77..631018f55cd 100644 --- a/homeassistant/components/tof/sensor.py +++ b/homeassistant/components/tof/sensor.py @@ -82,12 +82,12 @@ class VL53L1XSensor(SensorEntity): return self._name @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit of measurement.""" return self._unit_of_measurement diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py index b16672674af..8d298c4a865 100644 --- a/homeassistant/components/toon/sensor.py +++ b/homeassistant/components/toon/sensor.py @@ -130,7 +130,7 @@ class ToonSensor(ToonEntity, SensorEntity): self._attr_last_reset = sensor.get(ATTR_LAST_RESET) self._attr_name = sensor[ATTR_NAME] self._attr_state_class = sensor.get(ATTR_STATE_CLASS) - self._attr_unit_of_measurement = sensor[ATTR_UNIT_OF_MEASUREMENT] + self._attr_native_unit_of_measurement = sensor[ATTR_UNIT_OF_MEASUREMENT] self._attr_device_class = sensor.get(ATTR_DEVICE_CLASS) self._attr_unique_id = ( # This unique ID is a bit ugly and contains unneeded information. @@ -139,7 +139,7 @@ class ToonSensor(ToonEntity, SensorEntity): ) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" section = getattr( self.coordinator.data, SENSOR_ENTITIES[self.key][ATTR_SECTION] diff --git a/homeassistant/components/torque/sensor.py b/homeassistant/components/torque/sensor.py index 8e3053d9bd8..162dd5f437c 100644 --- a/homeassistant/components/torque/sensor.py +++ b/homeassistant/components/torque/sensor.py @@ -120,12 +120,12 @@ class TorqueSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 1f028849d32..f7f68b666ba 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -30,7 +30,7 @@ class TradfriSensor(TradfriBaseDevice, SensorEntity): """The platform class required by Home Assistant.""" _attr_device_class = DEVICE_CLASS_BATTERY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, device, api, gateway_id): """Initialize the device.""" @@ -38,6 +38,6 @@ class TradfriSensor(TradfriBaseDevice, SensorEntity): self._unique_id = f"{gateway_id}-{device.id}" @property - def state(self): + def native_value(self): """Return the current state of the device.""" return self._device.device_info.battery_level diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index 5e541045266..cd5cdf29521 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -189,7 +189,7 @@ class TrainSensor(SensorEntity): return ICON @property - def state(self): + def native_value(self): """Return the departure state.""" state = self._state if state is not None: diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 1ae090ea231..1435da6a988 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -185,12 +185,12 @@ class TrafikverketWeatherStation(SensorEntity): return self._device_class @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index b00ccfc68c0..e5f827d1e52 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -62,7 +62,7 @@ class TransmissionSensor(SensorEntity): return f"{self._tm_client.api.host}-{self.name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -95,7 +95,7 @@ class TransmissionSpeedSensor(TransmissionSensor): """Representation of a Transmission speed sensor.""" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return DATA_RATE_MEGABYTES_PER_SECOND @@ -145,7 +145,7 @@ class TransmissionTorrentsSensor(TransmissionSensor): } @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return "Torrents" diff --git a/homeassistant/components/transport_nsw/sensor.py b/homeassistant/components/transport_nsw/sensor.py index be76999ec3f..0ebb2b39cb8 100644 --- a/homeassistant/components/transport_nsw/sensor.py +++ b/homeassistant/components/transport_nsw/sensor.py @@ -81,7 +81,7 @@ class TransportNSWSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -101,7 +101,7 @@ class TransportNSWSensor(SensorEntity): } @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return TIME_MINUTES diff --git a/homeassistant/components/travisci/sensor.py b/homeassistant/components/travisci/sensor.py index 82b158aa0ec..c4c68197677 100644 --- a/homeassistant/components/travisci/sensor.py +++ b/homeassistant/components/travisci/sensor.py @@ -113,12 +113,12 @@ class TravisCISensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return SENSOR_TYPES[self._sensor_type][1] @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index cc17bf6f1a2..0069c3db93c 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -132,7 +132,7 @@ class TwenteMilieuSensor(SensorEntity): self.async_schedule_update_ha_state(True) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py index cfabcf1045f..15581e11c28 100644 --- a/homeassistant/components/twitch/sensor.py +++ b/homeassistant/components/twitch/sensor.py @@ -77,7 +77,7 @@ class TwitchSensor(SensorEntity): return self._channel.display_name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index c66db9bb24b..69e4f0df99b 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -93,7 +93,7 @@ class UkTransportSensor(SensorEntity): TRANSPORT_API_URL_BASE = "https://transportapi.com/v3/uk/" _attr_icon = "mdi:train" - _attr_unit_of_measurement = TIME_MINUTES + _attr_native_unit_of_measurement = TIME_MINUTES def __init__(self, name, api_app_id, api_app_key, url): """Initialize the sensor.""" @@ -110,7 +110,7 @@ class UkTransportSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 338f695a2b4..6a009415163 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -86,7 +86,7 @@ class UniFiBandwidthSensor(UniFiClient, SensorEntity): DOMAIN = DOMAIN - _attr_unit_of_measurement = DATA_MEGABYTES + _attr_native_unit_of_measurement = DATA_MEGABYTES @property def name(self) -> str: @@ -105,7 +105,7 @@ class UniFiRxBandwidthSensor(UniFiBandwidthSensor): TYPE = RX_SENSOR @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" if self._is_wired: return self.client.wired_rx_bytes / 1000000 @@ -118,7 +118,7 @@ class UniFiTxBandwidthSensor(UniFiBandwidthSensor): TYPE = TX_SENSOR @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" if self._is_wired: return self.client.wired_tx_bytes / 1000000 @@ -167,7 +167,7 @@ class UniFiUpTimeSensor(UniFiClient, SensorEntity): return f"{super().name} {self.TYPE.capitalize()}" @property - def state(self) -> datetime: + def native_value(self) -> datetime: """Return the uptime of the client.""" if self.client.uptime < 1000000000: return (dt_util.now() - timedelta(seconds=self.client.uptime)).isoformat() diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 54744490a86..82df1f59469 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -161,7 +161,7 @@ class UpnpSensor(CoordinatorEntity, SensorEntity): return f"{self._device.udn}_{self._sensor_type['unique_id']}" @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" return self._sensor_type["unit"] @@ -180,7 +180,7 @@ class RawUpnpSensor(UpnpSensor): """Representation of a UPnP/IGD sensor.""" @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the device.""" device_value_key = self._sensor_type["device_value_key"] value = self.coordinator.data[device_value_key] @@ -209,7 +209,7 @@ class DerivedUpnpSensor(UpnpSensor): return f"{self._device.udn}_{self._sensor_type['derived_unique_id']}" @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" return self._sensor_type["derived_unit"] @@ -218,7 +218,7 @@ class DerivedUpnpSensor(UpnpSensor): return current_value < self._last_value @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the device.""" # Can't calculate any derivative if we have only one value. device_value_key = self._sensor_type["device_value_key"] diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py index 5b31b2e81d0..db06b09ea18 100644 --- a/homeassistant/components/uptime/sensor.py +++ b/homeassistant/components/uptime/sensor.py @@ -50,4 +50,4 @@ class UptimeSensor(SensorEntity): self._attr_name: str = name self._attr_device_class: str = DEVICE_CLASS_TIMESTAMP self._attr_should_poll: bool = False - self._attr_state: str = dt_util.now().isoformat() + self._attr_native_value: str = dt_util.now().isoformat() diff --git a/homeassistant/components/uscis/sensor.py b/homeassistant/components/uscis/sensor.py index bd261aba4fb..c0c2d1ae165 100644 --- a/homeassistant/components/uscis/sensor.py +++ b/homeassistant/components/uscis/sensor.py @@ -54,7 +54,7 @@ class UscisSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state.""" return self._state diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 509c0562f97..1ff201aaceb 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -321,7 +321,7 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -336,7 +336,7 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): return STATE_CLASS_MEASUREMENT @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement From e23750b2a4eb474677d4bd555713f0c426c56e95 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 11 Aug 2021 18:58:19 +0200 Subject: [PATCH 163/355] Add device class `gas` and enable statistics for it (#54110) Co-authored-by: Martin Hjelmare Co-authored-by: Erik Montnemery --- homeassistant/components/dsmr/const.py | 4 ++ homeassistant/components/dsmr/sensor.py | 14 ++++++- .../components/recorder/statistics.py | 17 ++++++++- homeassistant/components/sensor/__init__.py | 2 + .../components/sensor/device_condition.py | 4 ++ .../components/sensor/device_trigger.py | 4 ++ homeassistant/components/sensor/recorder.py | 11 ++++++ homeassistant/components/sensor/strings.json | 2 + homeassistant/components/toon/const.py | 18 +++++---- homeassistant/const.py | 1 + homeassistant/util/volume.py | 26 +++++++++++-- tests/components/dsmr/test_sensor.py | 21 ++++++----- .../components/sensor/test_device_trigger.py | 2 +- tests/components/sensor/test_recorder.py | 37 ++++++++++++------- .../custom_components/test/sensor.py | 2 + tests/util/test_volume.py | 20 ++++++++++ 16 files changed, 146 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index a5e51816183..b5fb74bbbe6 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -9,6 +9,7 @@ from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT from homeassistant.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, ) @@ -256,6 +257,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( is_gas=True, force_update=True, icon="mdi:fire", + device_class=DEVICE_CLASS_GAS, last_reset=dt.utc_from_timestamp(0), state_class=STATE_CLASS_MEASUREMENT, ), @@ -266,6 +268,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( is_gas=True, force_update=True, icon="mdi:fire", + device_class=DEVICE_CLASS_GAS, last_reset=dt.utc_from_timestamp(0), state_class=STATE_CLASS_MEASUREMENT, ), @@ -276,6 +279,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( is_gas=True, force_update=True, icon="mdi:fire", + device_class=DEVICE_CLASS_GAS, last_reset=dt.utc_from_timestamp(0), state_class=STATE_CLASS_MEASUREMENT, ), diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index ae3fb6b01a4..dbc29144719 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -16,7 +16,12 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + EVENT_HOMEASSISTANT_STOP, + VOLUME_CUBIC_METERS, +) from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -56,6 +61,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +UNIT_CONVERSION = {"m3": VOLUME_CUBIC_METERS} + async def async_setup_platform( hass: HomeAssistant, @@ -260,7 +267,10 @@ class DSMREntity(SensorEntity): @property def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" - return self.get_dsmr_object_attr("unit") + unit_of_measurement = self.get_dsmr_object_attr("unit") + if unit_of_measurement in UNIT_CONVERSION: + return UNIT_CONVERSION[unit_of_measurement] + return unit_of_measurement @staticmethod def translate_tariff(value: str, dsmr_version: str) -> str | None: diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index f3b0b27df39..b91e4d160df 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -11,13 +11,19 @@ from sqlalchemy import bindparam from sqlalchemy.ext import baked from sqlalchemy.orm.scoping import scoped_session -from homeassistant.const import PRESSURE_PA, TEMP_CELSIUS +from homeassistant.const import ( + PRESSURE_PA, + TEMP_CELSIUS, + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, +) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import entity_registry 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 DOMAIN from .models import ( @@ -64,6 +70,11 @@ UNIT_CONVERSIONS = { ) if x is not None else None, + VOLUME_CUBIC_METERS: lambda x, units: volume_util.convert( + x, VOLUME_CUBIC_METERS, _configured_unit(VOLUME_CUBIC_METERS, units) + ) + if x is not None + else None, } _LOGGER = logging.getLogger(__name__) @@ -214,6 +225,10 @@ def _configured_unit(unit: str, units: UnitSystem) -> str: return units.pressure_unit if unit == TEMP_CELSIUS: return units.temperature_unit + if unit == VOLUME_CUBIC_METERS: + if units.is_metric: + return VOLUME_CUBIC_METERS + return VOLUME_CUBIC_FEET return unit diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index c88b7da13f4..483d8b88f2e 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -17,6 +17,7 @@ from homeassistant.const import ( DEVICE_CLASS_CO2, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_MONETARY, @@ -65,6 +66,7 @@ DEVICE_CLASSES: Final[list[str]] = [ DEVICE_CLASS_POWER, # power (W/kW) DEVICE_CLASS_POWER_FACTOR, # power factor (%) DEVICE_CLASS_VOLTAGE, # voltage (V) + DEVICE_CLASS_GAS, # gas (m³ or ft³) ] DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index a77ed2d2cd7..3b9f3839cfb 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -16,6 +16,7 @@ from homeassistant.const import ( DEVICE_CLASS_CO2, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, @@ -46,6 +47,7 @@ CONF_IS_CO2 = "is_carbon_dioxide" CONF_IS_CURRENT = "is_current" CONF_IS_ENERGY = "is_energy" CONF_IS_HUMIDITY = "is_humidity" +CONF_IS_GAS = "is_gas" CONF_IS_ILLUMINANCE = "is_illuminance" CONF_IS_POWER = "is_power" CONF_IS_POWER_FACTOR = "is_power_factor" @@ -61,6 +63,7 @@ ENTITY_CONDITIONS = { 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_GAS: [{CONF_TYPE: CONF_IS_GAS}], DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_IS_HUMIDITY}], DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_IS_ILLUMINANCE}], DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_IS_POWER}], @@ -83,6 +86,7 @@ CONDITION_SCHEMA = vol.All( CONF_IS_CO2, CONF_IS_CURRENT, CONF_IS_ENERGY, + CONF_IS_GAS, CONF_IS_HUMIDITY, CONF_IS_ILLUMINANCE, CONF_IS_POWER, diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 3b00bae816d..f7d72dd4c1b 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -19,6 +19,7 @@ from homeassistant.const import ( DEVICE_CLASS_CO2, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, @@ -44,6 +45,7 @@ CONF_CO = "carbon_monoxide" CONF_CO2 = "carbon_dioxide" CONF_CURRENT = "current" CONF_ENERGY = "energy" +CONF_GAS = "gas" CONF_HUMIDITY = "humidity" CONF_ILLUMINANCE = "illuminance" CONF_POWER = "power" @@ -60,6 +62,7 @@ ENTITY_TRIGGERS = { DEVICE_CLASS_CO2: [{CONF_TYPE: CONF_CO2}], DEVICE_CLASS_CURRENT: [{CONF_TYPE: CONF_CURRENT}], DEVICE_CLASS_ENERGY: [{CONF_TYPE: CONF_ENERGY}], + DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_GAS}], DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_HUMIDITY}], DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_ILLUMINANCE}], DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWER}], @@ -83,6 +86,7 @@ TRIGGER_SCHEMA = vol.All( CONF_CO2, CONF_CURRENT, CONF_ENERGY, + CONF_GAS, CONF_HUMIDITY, CONF_ILLUMINANCE, CONF_POWER, diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index afcfe2f228d..fb7393cfe1d 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -11,6 +11,7 @@ from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DEVICE_CLASS_BATTERY, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_MONETARY, DEVICE_CLASS_PRESSURE, @@ -35,11 +36,14 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN, + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, ) from homeassistant.core import HomeAssistant, State import homeassistant.util.dt as dt_util 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 @@ -53,6 +57,7 @@ DEVICE_CLASS_OR_UNIT_STATISTICS = { DEVICE_CLASS_POWER: {"mean", "min", "max"}, DEVICE_CLASS_PRESSURE: {"mean", "min", "max"}, DEVICE_CLASS_TEMPERATURE: {"mean", "min", "max"}, + DEVICE_CLASS_GAS: {"sum"}, PERCENTAGE: {"mean", "min", "max"}, } @@ -62,6 +67,7 @@ DEVICE_CLASS_UNITS = { DEVICE_CLASS_POWER: POWER_WATT, DEVICE_CLASS_PRESSURE: PRESSURE_PA, DEVICE_CLASS_TEMPERATURE: TEMP_CELSIUS, + DEVICE_CLASS_GAS: VOLUME_CUBIC_METERS, } UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { @@ -92,6 +98,11 @@ UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { TEMP_FAHRENHEIT: temperature_util.fahrenheit_to_celsius, TEMP_KELVIN: temperature_util.kelvin_to_celsius, }, + # Convert volume to cubic meter + DEVICE_CLASS_GAS: { + VOLUME_CUBIC_METERS: lambda x: x, + VOLUME_CUBIC_FEET: volume_util.cubic_feet_to_cubic_meter, + }, } # Keep track of entities for which a warning about unsupported unit has been logged diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index efe5366cfec..54d0f9ad76c 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -5,6 +5,7 @@ "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", + "is_gas": "Current {entity_name} gas", "is_humidity": "Current {entity_name} humidity", "is_illuminance": "Current {entity_name} illuminance", "is_power": "Current {entity_name} power", @@ -21,6 +22,7 @@ "battery_level": "{entity_name} battery level changes", "carbon_monoxide": "{entity_name} carbon monoxide concentration changes", "carbon_dioxide": "{entity_name} carbon dioxide concentration changes", + "gas": "{entity_name} gas changes", "humidity": "{entity_name} humidity changes", "illuminance": "{entity_name} illuminance changes", "power": "{entity_name} power changes", diff --git a/homeassistant/components/toon/const.py b/homeassistant/components/toon/const.py index 4af57e03412..1c9192c4544 100644 --- a/homeassistant/components/toon/const.py +++ b/homeassistant/components/toon/const.py @@ -18,10 +18,12 @@ from homeassistant.const import ( ATTR_ICON, ATTR_NAME, ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_GAS, ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_WATT, TEMP_CELSIUS, + VOLUME_CUBIC_METERS, ) from homeassistant.util import dt as dt_util @@ -38,7 +40,6 @@ DEFAULT_MIN_TEMP = 6.0 CURRENCY_EUR = "EUR" VOLUME_CM3 = "CM3" -VOLUME_M3 = "M3" VOLUME_LHOUR = "L/H" VOLUME_LMIN = "L/MIN" @@ -125,7 +126,8 @@ SENSOR_ENTITIES = { ATTR_NAME: "Average Daily Gas Usage", ATTR_SECTION: "gas_usage", ATTR_MEASUREMENT: "day_average", - ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, + ATTR_DEVICE_CLASS: DEVICE_CLASS_GAS, + ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, ATTR_ICON: "mdi:gas-cylinder", ATTR_DEFAULT_ENABLED: False, }, @@ -133,7 +135,8 @@ SENSOR_ENTITIES = { ATTR_NAME: "Gas Usage Today", ATTR_SECTION: "gas_usage", ATTR_MEASUREMENT: "day_usage", - ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, + ATTR_DEVICE_CLASS: DEVICE_CLASS_GAS, + ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, ATTR_ICON: "mdi:gas-cylinder", }, "gas_daily_cost": { @@ -147,9 +150,10 @@ SENSOR_ENTITIES = { ATTR_NAME: "Gas Meter", ATTR_SECTION: "gas_usage", ATTR_MEASUREMENT: "meter", - ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, + ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, ATTR_ICON: "mdi:gas-cylinder", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_GAS, ATTR_LAST_RESET: dt_util.utc_from_timestamp(0), ATTR_DEFAULT_ENABLED: False, }, @@ -321,7 +325,7 @@ SENSOR_ENTITIES = { ATTR_NAME: "Average Daily Water Usage", ATTR_SECTION: "water_usage", ATTR_MEASUREMENT: "day_average", - ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, + ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, ATTR_ICON: "mdi:water", ATTR_DEFAULT_ENABLED: False, }, @@ -329,7 +333,7 @@ SENSOR_ENTITIES = { ATTR_NAME: "Water Usage Today", ATTR_SECTION: "water_usage", ATTR_MEASUREMENT: "day_usage", - ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, + ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, ATTR_ICON: "mdi:water", ATTR_DEFAULT_ENABLED: False, }, @@ -337,7 +341,7 @@ SENSOR_ENTITIES = { ATTR_NAME: "Water Meter", ATTR_SECTION: "water_usage", ATTR_MEASUREMENT: "meter", - ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, + ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, ATTR_ICON: "mdi:water", ATTR_DEFAULT_ENABLED: False, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, diff --git a/homeassistant/const.py b/homeassistant/const.py index 5f4f8cd084c..9fa5c2cd231 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -247,6 +247,7 @@ DEVICE_CLASS_SIGNAL_STRENGTH: Final = "signal_strength" DEVICE_CLASS_TEMPERATURE: Final = "temperature" DEVICE_CLASS_TIMESTAMP: Final = "timestamp" DEVICE_CLASS_VOLTAGE: Final = "voltage" +DEVICE_CLASS_GAS: Final = "gas" # #### STATES #### STATE_ON: Final = "on" diff --git a/homeassistant/util/volume.py b/homeassistant/util/volume.py index f4a02dbe82e..84a3faa0951 100644 --- a/homeassistant/util/volume.py +++ b/homeassistant/util/volume.py @@ -6,6 +6,8 @@ from numbers import Number from homeassistant.const import ( UNIT_NOT_RECOGNIZED_TEMPLATE, VOLUME, + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, VOLUME_FLUID_OUNCE, VOLUME_GALLONS, VOLUME_LITERS, @@ -17,19 +19,31 @@ VALID_UNITS: tuple[str, ...] = ( VOLUME_MILLILITERS, VOLUME_GALLONS, VOLUME_FLUID_OUNCE, + VOLUME_CUBIC_METERS, + VOLUME_CUBIC_FEET, ) -def __liter_to_gallon(liter: float) -> float: +def liter_to_gallon(liter: float) -> float: """Convert a volume measurement in Liter to Gallon.""" return liter * 0.2642 -def __gallon_to_liter(gallon: float) -> float: +def gallon_to_liter(gallon: float) -> float: """Convert a volume measurement in Gallon to Liter.""" return gallon * 3.785 +def cubic_meter_to_cubic_feet(cubic_meter: float) -> float: + """Convert a volume measurement in cubic meter to cubic feet.""" + return cubic_meter * 35.3146667 + + +def cubic_feet_to_cubic_meter(cubic_feet: float) -> float: + """Convert a volume measurement in cubic feet to cubic meter.""" + return cubic_feet * 0.0283168466 + + def convert(volume: float, from_unit: str, to_unit: str) -> float: """Convert a temperature from one unit to another.""" if from_unit not in VALID_UNITS: @@ -45,8 +59,12 @@ def convert(volume: float, from_unit: str, to_unit: str) -> float: result: float = volume if from_unit == VOLUME_LITERS and to_unit == VOLUME_GALLONS: - result = __liter_to_gallon(volume) + result = liter_to_gallon(volume) elif from_unit == VOLUME_GALLONS and to_unit == VOLUME_LITERS: - result = __gallon_to_liter(volume) + result = gallon_to_liter(volume) + elif from_unit == VOLUME_CUBIC_METERS and to_unit == VOLUME_CUBIC_FEET: + result = cubic_meter_to_cubic_feet(volume) + elif from_unit == VOLUME_CUBIC_FEET and to_unit == VOLUME_CUBIC_METERS: + result = cubic_feet_to_cubic_meter(volume) return result diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 90194eaeb6b..c7e0addd800 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -24,6 +24,7 @@ from homeassistant.const import ( ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, STATE_UNKNOWN, @@ -104,7 +105,7 @@ async def test_default_setup(hass, dsmr_connection_fixture): GAS_METER_READING: MBusObject( [ {"value": datetime.datetime.fromtimestamp(1551642213)}, - {"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, + {"value": Decimal(745.695), "unit": "m3"}, ] ), } @@ -164,7 +165,7 @@ async def test_default_setup(hass, dsmr_connection_fixture): # check if gas consumption is parsed correctly gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" - assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT @@ -228,7 +229,7 @@ async def test_v4_meter(hass, dsmr_connection_fixture): HOURLY_GAS_METER_READING: MBusObject( [ {"value": datetime.datetime.fromtimestamp(1551642213)}, - {"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, + {"value": Decimal(745.695), "unit": "m3"}, ] ), ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), @@ -263,8 +264,8 @@ async def test_v4_meter(hass, dsmr_connection_fixture): # check if gas consumption is parsed correctly gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS - assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT @@ -299,7 +300,7 @@ async def test_v5_meter(hass, dsmr_connection_fixture): HOURLY_GAS_METER_READING: MBusObject( [ {"value": datetime.datetime.fromtimestamp(1551642213)}, - {"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, + {"value": Decimal(745.695), "unit": "m3"}, ] ), ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), @@ -334,7 +335,7 @@ async def test_v5_meter(hass, dsmr_connection_fixture): # check if gas consumption is parsed correctly gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" - assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT @@ -370,7 +371,7 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture): HOURLY_GAS_METER_READING: MBusObject( [ {"value": datetime.datetime.fromtimestamp(1551642213)}, - {"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, + {"value": Decimal(745.695), "unit": "m3"}, ] ), LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL: CosemObject( @@ -415,7 +416,7 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture): # check if gas consumption is parsed correctly gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" - assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT @@ -450,7 +451,7 @@ async def test_belgian_meter(hass, dsmr_connection_fixture): BELGIUM_HOURLY_GAS_METER_READING: MBusObject( [ {"value": datetime.datetime.fromtimestamp(1551642213)}, - {"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, + {"value": Decimal(745.695), "unit": "m3"}, ] ), ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), @@ -485,7 +486,7 @@ async def test_belgian_meter(hass, dsmr_connection_fixture): # check if gas consumption is parsed correctly gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" - assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is DEVICE_CLASS_GAS assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index ce35e2506a9..f955c3c19db 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -86,7 +86,7 @@ async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrat if device_class != "none" ] triggers = await async_get_device_automations(hass, "trigger", device_entry.id) - assert len(triggers) == 13 + assert len(triggers) == 14 assert triggers == expected_triggers diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 58614e86a0e..a612bc75a77 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -39,6 +39,11 @@ TEMPERATURE_SENSOR_ATTRIBUTES = { "state_class": "measurement", "unit_of_measurement": "°C", } +GAS_SENSOR_ATTRIBUTES = { + "device_class": "gas", + "state_class": "measurement", + "unit_of_measurement": "m³", +} @pytest.mark.parametrize( @@ -154,11 +159,13 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes [ ("energy", "kWh", "kWh", 1), ("energy", "Wh", "kWh", 1 / 1000), - ("monetary", "€", "€", 1), + ("monetary", "EUR", "EUR", 1), ("monetary", "SEK", "SEK", 1), + ("gas", "m³", "m³", 1), + ("gas", "ft³", "m³", 0.0283168466), ], ) -def test_compile_hourly_energy_statistics( +def test_compile_hourly_sum_statistics( hass_recorder, caplog, device_class, unit, native_unit, factor ): """Test compiling hourly statistics.""" @@ -174,7 +181,7 @@ def test_compile_hourly_energy_statistics( } seq = [10, 15, 20, 10, 30, 40, 50, 60, 70] - four, eight, states = record_energy_states( + four, eight, states = record_meter_states( hass, zero, "sensor.test1", attributes, seq ) hist = history.get_significant_states( @@ -254,14 +261,14 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): seq3 = [0, 0, 5, 10, 30, 50, 60, 80, 90] seq4 = [0, 0, 5, 10, 30, 50, 60, 80, 90] - four, eight, states = record_energy_states( + four, eight, states = record_meter_states( hass, zero, "sensor.test1", sns1_attr, seq1 ) - _, _, _states = record_energy_states(hass, zero, "sensor.test2", sns2_attr, seq2) + _, _, _states = record_meter_states(hass, zero, "sensor.test2", sns2_attr, seq2) states = {**states, **_states} - _, _, _states = record_energy_states(hass, zero, "sensor.test3", sns3_attr, seq3) + _, _, _states = record_meter_states(hass, zero, "sensor.test3", sns3_attr, seq3) states = {**states, **_states} - _, _, _states = record_energy_states(hass, zero, "sensor.test4", sns4_attr, seq4) + _, _, _states = record_meter_states(hass, zero, "sensor.test4", sns4_attr, seq4) states = {**states, **_states} hist = history.get_significant_states( @@ -336,14 +343,14 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): seq3 = [0, 0, 5, 10, 30, 50, 60, 80, 90] seq4 = [0, 0, 5, 10, 30, 50, 60, 80, 90] - four, eight, states = record_energy_states( + four, eight, states = record_meter_states( hass, zero, "sensor.test1", sns1_attr, seq1 ) - _, _, _states = record_energy_states(hass, zero, "sensor.test2", sns2_attr, seq2) + _, _, _states = record_meter_states(hass, zero, "sensor.test2", sns2_attr, seq2) states = {**states, **_states} - _, _, _states = record_energy_states(hass, zero, "sensor.test3", sns3_attr, seq3) + _, _, _states = record_meter_states(hass, zero, "sensor.test3", sns3_attr, seq3) states = {**states, **_states} - _, _, _states = record_energy_states(hass, zero, "sensor.test4", sns4_attr, seq4) + _, _, _states = record_meter_states(hass, zero, "sensor.test4", sns4_attr, seq4) states = {**states, **_states} hist = history.get_significant_states( hass, zero - timedelta.resolution, eight + timedelta.resolution @@ -632,6 +639,8 @@ def test_compile_hourly_statistics_fails(hass_recorder, caplog): ("humidity", None, None, "mean"), ("monetary", "USD", "USD", "sum"), ("monetary", "None", "None", "sum"), + ("gas", "m³", "m³", "sum"), + ("gas", "ft³", "m³", "sum"), ("pressure", "Pa", "Pa", "mean"), ("pressure", "hPa", "Pa", "mean"), ("pressure", "mbar", "Pa", "mean"), @@ -697,7 +706,7 @@ def test_list_statistic_ids_unsupported(hass_recorder, caplog, _attributes): def record_states(hass, zero, entity_id, attributes): """Record some test states. - We inject a bunch of state updates for temperature sensors. + We inject a bunch of state updates for measurement sensors. """ attributes = dict(attributes) @@ -725,10 +734,10 @@ def record_states(hass, zero, entity_id, attributes): return four, states -def record_energy_states(hass, zero, entity_id, _attributes, seq): +def record_meter_states(hass, zero, entity_id, _attributes, seq): """Record some test states. - We inject a bunch of state updates for energy sensors. + We inject a bunch of state updates for meter sensors. """ def set_state(entity_id, state, **kwargs): diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index 7c121d1c05a..f4b2e96321e 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -9,6 +9,7 @@ from homeassistant.const import ( PERCENTAGE, PRESSURE_HPA, SIGNAL_STRENGTH_DECIBELS, + VOLUME_CUBIC_METERS, ) from tests.common import MockEntity @@ -30,6 +31,7 @@ UNITS_OF_MEASUREMENT = { sensor.DEVICE_CLASS_ENERGY: "kWh", # energy (Wh/kWh) sensor.DEVICE_CLASS_POWER_FACTOR: PERCENTAGE, # power factor (no unit, min: -1.0, max: 1.0) sensor.DEVICE_CLASS_VOLTAGE: "V", # voltage (V) + sensor.DEVICE_CLASS_GAS: VOLUME_CUBIC_METERS, # gas (m³) } ENTITIES = {} diff --git a/tests/util/test_volume.py b/tests/util/test_volume.py index 2c596d92e5b..3cbf5b72130 100644 --- a/tests/util/test_volume.py +++ b/tests/util/test_volume.py @@ -3,6 +3,8 @@ import pytest from homeassistant.const import ( + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, VOLUME_FLUID_OUNCE, VOLUME_GALLONS, VOLUME_LITERS, @@ -47,3 +49,21 @@ def test_convert_from_gallons(): """Test conversion from gallons to other units.""" gallons = 5 assert volume_util.convert(gallons, VOLUME_GALLONS, VOLUME_LITERS) == 18.925 + + +def test_convert_from_cubic_meters(): + """Test conversion from cubic meter to other units.""" + cubic_meters = 5 + assert ( + volume_util.convert(cubic_meters, VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET) + == 176.5733335 + ) + + +def test_convert_from_cubic_feet(): + """Test conversion from cubic feet to cubic meters to other units.""" + cubic_feets = 500 + assert ( + volume_util.convert(cubic_feets, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS) + == 14.1584233 + ) From ae507aeed1b26fdec6302680b6b6afff7862c770 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Aug 2021 21:17:16 +0200 Subject: [PATCH 164/355] Move temperature conversions to sensor base class (8/8) (#54483) * Move temperature conversions to entity base class (8/8) * Fix wallbox sensor * Fix tests --- homeassistant/components/vallox/sensor.py | 4 ++-- homeassistant/components/vasttrafik/sensor.py | 2 +- homeassistant/components/velbus/sensor.py | 4 ++-- homeassistant/components/vera/sensor.py | 4 ++-- homeassistant/components/verisure/sensor.py | 12 +++++----- homeassistant/components/versasense/sensor.py | 4 ++-- homeassistant/components/version/sensor.py | 2 +- .../components/viaggiatreno/sensor.py | 4 ++-- homeassistant/components/vicare/sensor.py | 4 ++-- homeassistant/components/vilfo/sensor.py | 4 ++-- .../components/volkszaehler/sensor.py | 4 ++-- .../components/volvooncall/sensor.py | 4 ++-- homeassistant/components/vultr/sensor.py | 4 ++-- homeassistant/components/wallbox/sensor.py | 8 +++---- homeassistant/components/waqi/sensor.py | 4 ++-- .../components/waterfurnace/sensor.py | 4 ++-- .../components/waze_travel_time/sensor.py | 4 ++-- .../components/websocket_api/sensor.py | 4 ++-- homeassistant/components/whois/sensor.py | 4 ++-- homeassistant/components/wiffi/sensor.py | 6 ++--- homeassistant/components/wink/sensor.py | 4 ++-- .../components/wirelesstag/sensor.py | 4 ++-- homeassistant/components/withings/sensor.py | 4 ++-- homeassistant/components/wled/sensor.py | 22 ++++++++--------- homeassistant/components/wolflink/sensor.py | 12 +++++----- homeassistant/components/worldclock/sensor.py | 2 +- .../components/worldtidesinfo/sensor.py | 2 +- .../components/worxlandroid/sensor.py | 4 ++-- homeassistant/components/wsdot/sensor.py | 4 ++-- homeassistant/components/xbee/__init__.py | 4 ++-- homeassistant/components/xbee/sensor.py | 4 ++-- homeassistant/components/xbox/sensor.py | 2 +- homeassistant/components/xbox_live/sensor.py | 2 +- .../components/xiaomi_aqara/sensor.py | 8 +++---- .../components/xiaomi_miio/sensor.py | 24 +++++++++---------- homeassistant/components/xs1/sensor.py | 4 ++-- .../components/yandex_transport/sensor.py | 2 +- homeassistant/components/zabbix/sensor.py | 4 ++-- homeassistant/components/zamg/sensor.py | 4 ++-- homeassistant/components/zestimate/sensor.py | 2 +- homeassistant/components/zha/sensor.py | 6 ++--- homeassistant/components/zodiac/sensor.py | 2 +- homeassistant/components/zoneminder/sensor.py | 8 +++---- homeassistant/components/zwave/sensor.py | 8 +++---- homeassistant/components/zwave_js/sensor.py | 16 ++++++------- tests/components/vultr/test_sensor.py | 1 + tests/components/wsdot/test_sensor.py | 3 +++ tests/components/zwave/test_sensor.py | 18 ++++++++++---- 48 files changed, 141 insertions(+), 129 deletions(-) diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index ddfb9d1a7d3..836931f089e 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -141,7 +141,7 @@ class ValloxSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement @@ -161,7 +161,7 @@ class ValloxSensor(SensorEntity): return self._available @property - def state(self): + def native_value(self): """Return the state.""" return self._state diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py index 31c5da097ff..4c1c1de5e52 100644 --- a/homeassistant/components/vasttrafik/sensor.py +++ b/homeassistant/components/vasttrafik/sensor.py @@ -110,7 +110,7 @@ class VasttrafikDepartureSensor(SensorEntity): return self._attributes @property - def state(self): + def native_value(self): """Return the next departure time.""" return self._state diff --git a/homeassistant/components/velbus/sensor.py b/homeassistant/components/velbus/sensor.py index 9d9b68dd4eb..3a4aa2302f6 100644 --- a/homeassistant/components/velbus/sensor.py +++ b/homeassistant/components/velbus/sensor.py @@ -45,14 +45,14 @@ class VelbusSensor(VelbusEntity, SensorEntity): return self._module.get_class(self._channel) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._is_counter: return self._module.get_counter_state(self._channel) return self._module.get_state(self._channel) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" if self._is_counter: return self._module.get_counter_unit(self._channel) diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index 878f6ff376d..dd6d891c11d 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -53,12 +53,12 @@ class VeraSensor(VeraDevice[veraApi.VeraSensor], SensorEntity): self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property - def state(self) -> str: + def native_value(self) -> str: """Return the name of the sensor.""" return self.current_value @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR: diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index d39c235e9d5..cdeddd8d6e4 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -51,7 +51,7 @@ class VerisureThermometer(CoordinatorEntity, SensorEntity): coordinator: VerisureDataUpdateCoordinator _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_unit_of_measurement = TEMP_CELSIUS + _attr_native_unit_of_measurement = TEMP_CELSIUS def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str @@ -84,7 +84,7 @@ class VerisureThermometer(CoordinatorEntity, SensorEntity): } @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the entity.""" return self.coordinator.data["climate"][self.serial_number]["temperature"] @@ -104,7 +104,7 @@ class VerisureHygrometer(CoordinatorEntity, SensorEntity): coordinator: VerisureDataUpdateCoordinator _attr_device_class = DEVICE_CLASS_HUMIDITY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str @@ -137,7 +137,7 @@ class VerisureHygrometer(CoordinatorEntity, SensorEntity): } @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the entity.""" return self.coordinator.data["climate"][self.serial_number]["humidity"] @@ -156,7 +156,7 @@ class VerisureMouseDetection(CoordinatorEntity, SensorEntity): coordinator: VerisureDataUpdateCoordinator - _attr_unit_of_measurement = "Mice" + _attr_native_unit_of_measurement = "Mice" def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str @@ -186,7 +186,7 @@ class VerisureMouseDetection(CoordinatorEntity, SensorEntity): } @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the entity.""" return self.coordinator.data["mice"][self.serial_number]["detections"] diff --git a/homeassistant/components/versasense/sensor.py b/homeassistant/components/versasense/sensor.py index d29032af399..50982e92d12 100644 --- a/homeassistant/components/versasense/sensor.py +++ b/homeassistant/components/versasense/sensor.py @@ -65,12 +65,12 @@ class VSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index f20f2682986..1cd42cce9b3 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -151,5 +151,5 @@ class VersionSensor(SensorEntity): async def async_update(self): """Get the latest version information.""" await self.data.async_update() - self._attr_state = self.data.api.version + self._attr_native_value = self.data.api.version self._attr_extra_state_attributes = self.data.api.version_data diff --git a/homeassistant/components/viaggiatreno/sensor.py b/homeassistant/components/viaggiatreno/sensor.py index 10821859f9a..ddfbb9f20dd 100644 --- a/homeassistant/components/viaggiatreno/sensor.py +++ b/homeassistant/components/viaggiatreno/sensor.py @@ -103,7 +103,7 @@ class ViaggiaTrenoSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -113,7 +113,7 @@ class ViaggiaTrenoSensor(SensorEntity): return self._icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 4f7ab9df985..e96b3b8120a 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -369,12 +369,12 @@ class ViCareSensor(SensorEntity): return self._sensor[CONF_ICON] @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._sensor[CONF_UNIT_OF_MEASUREMENT] diff --git a/homeassistant/components/vilfo/sensor.py b/homeassistant/components/vilfo/sensor.py index 90527c60458..bb2df21f257 100644 --- a/homeassistant/components/vilfo/sensor.py +++ b/homeassistant/components/vilfo/sensor.py @@ -72,7 +72,7 @@ class VilfoRouterSensor(SensorEntity): return f"{parent_device_name} {sensor_name}" @property - def state(self): + def native_value(self): """Return the state.""" return self._state @@ -82,7 +82,7 @@ class VilfoRouterSensor(SensorEntity): return self._unique_id @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity.""" return SENSOR_TYPES[self.sensor_type].get(ATTR_UNIT) diff --git a/homeassistant/components/volkszaehler/sensor.py b/homeassistant/components/volkszaehler/sensor.py index 4eb2f512f31..21705d494d9 100644 --- a/homeassistant/components/volkszaehler/sensor.py +++ b/homeassistant/components/volkszaehler/sensor.py @@ -97,7 +97,7 @@ class VolkszaehlerSensor(SensorEntity): return SENSOR_TYPES[self.type][2] @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return SENSOR_TYPES[self.type][1] @@ -107,7 +107,7 @@ class VolkszaehlerSensor(SensorEntity): return self.vz_api.available @property - def state(self): + def native_value(self): """Return the state of the resources.""" return self._state diff --git a/homeassistant/components/volvooncall/sensor.py b/homeassistant/components/volvooncall/sensor.py index ad6571576b4..7a37713301e 100644 --- a/homeassistant/components/volvooncall/sensor.py +++ b/homeassistant/components/volvooncall/sensor.py @@ -15,11 +15,11 @@ class VolvoSensor(VolvoEntity, SensorEntity): """Representation of a Volvo sensor.""" @property - def state(self): + def native_value(self): """Return the state.""" return self.instrument.state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self.instrument.unit diff --git a/homeassistant/components/vultr/sensor.py b/homeassistant/components/vultr/sensor.py index 5e6815944d7..01506d4f47e 100644 --- a/homeassistant/components/vultr/sensor.py +++ b/homeassistant/components/vultr/sensor.py @@ -92,12 +92,12 @@ class VultrSensor(SensorEntity): return self._icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement to present the value in.""" return self._units @property - def state(self): + def native_value(self): """Return the value of this given sensor type.""" try: return round(float(self.data.get(self._condition)), 2) diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index 6d3ef952cbe..0691a39ff48 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -1,6 +1,6 @@ """Home Assistant component for accessing the Wallbox Portal API. The sensor component creates multiple sensors regarding wallbox performance.""" -from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import SensorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -28,7 +28,7 @@ async def async_setup_entry(hass, config, async_add_entities): ) -class WallboxSensor(CoordinatorEntity, Entity): +class WallboxSensor(CoordinatorEntity, SensorEntity): """Representation of the Wallbox portal.""" def __init__(self, coordinator, idx, ent, config): @@ -46,12 +46,12 @@ class WallboxSensor(CoordinatorEntity, Entity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self.coordinator.data[self._ent] @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of the sensor.""" return self._unit diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index 084ec17bb28..ed6013daa74 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -129,7 +129,7 @@ class WaqiSensor(SensorEntity): return "mdi:cloud" @property - def state(self): + def native_value(self): """Return the state of the device.""" if self._data is not None: return self._data.get("aqi") @@ -146,7 +146,7 @@ class WaqiSensor(SensorEntity): return self.uid @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return "AQI" diff --git a/homeassistant/components/waterfurnace/sensor.py b/homeassistant/components/waterfurnace/sensor.py index 8691cc4ed02..5d7832ca58d 100644 --- a/homeassistant/components/waterfurnace/sensor.py +++ b/homeassistant/components/waterfurnace/sensor.py @@ -101,7 +101,7 @@ class WaterFurnaceSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -111,7 +111,7 @@ class WaterFurnaceSensor(SensorEntity): return self._icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the units of measurement.""" return self._unit_of_measurement diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index b0168bbb44e..43265062998 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -202,7 +202,7 @@ class WazeTravelTime(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._waze_data.duration is not None: return round(self._waze_data.duration) @@ -210,7 +210,7 @@ class WazeTravelTime(SensorEntity): return None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return TIME_MINUTES diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py index 60d42e97604..d6f27aff6ae 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -53,12 +53,12 @@ class APICount(SensorEntity): return "Connected clients" @property - def state(self) -> int: + def native_value(self) -> int: """Return current API count.""" return self.count @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit of measurement.""" return "clients" diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 4219c80193d..5d5e595fa50 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -70,12 +70,12 @@ class WhoisSensor(SensorEntity): return "mdi:calendar-clock" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement to present the value in.""" return TIME_DAYS @property - def state(self): + def native_value(self): """Return the expiration days for hostname.""" return self._state diff --git a/homeassistant/components/wiffi/sensor.py b/homeassistant/components/wiffi/sensor.py index 800a420f8f0..b9bcd317b46 100644 --- a/homeassistant/components/wiffi/sensor.py +++ b/homeassistant/components/wiffi/sensor.py @@ -78,12 +78,12 @@ class NumberEntity(WiffiEntity, SensorEntity): return self._device_class @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the value of the entity.""" return self._value @@ -111,7 +111,7 @@ class StringEntity(WiffiEntity, SensorEntity): self.reset_expiration_date() @property - def state(self): + def native_value(self): """Return the value of the entity.""" return self._value diff --git a/homeassistant/components/wink/sensor.py b/homeassistant/components/wink/sensor.py index f640a24def2..86199f44e91 100644 --- a/homeassistant/components/wink/sensor.py +++ b/homeassistant/components/wink/sensor.py @@ -62,7 +62,7 @@ class WinkSensorEntity(WinkDevice, SensorEntity): self.hass.data[DOMAIN]["entities"]["sensor"].append(self) @property - def state(self): + def native_value(self): """Return the state.""" state = None if self.capability == "humidity": @@ -82,7 +82,7 @@ class WinkSensorEntity(WinkDevice, SensorEntity): return state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/wirelesstag/sensor.py b/homeassistant/components/wirelesstag/sensor.py index de70efda424..7ad0a7f52c2 100644 --- a/homeassistant/components/wirelesstag/sensor.py +++ b/homeassistant/components/wirelesstag/sensor.py @@ -83,7 +83,7 @@ class WirelessTagSensor(WirelessTagBaseSensor, SensorEntity): return self.name.lower().replace(" ", "_") @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -93,7 +93,7 @@ class WirelessTagSensor(WirelessTagBaseSensor, SensorEntity): return self._sensor_type @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._sensor.unit diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index ca7391eb58e..0ca40d28440 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -30,11 +30,11 @@ class WithingsHealthSensor(BaseWithingsSensor, SensorEntity): """Implementation of a Withings sensor.""" @property - def state(self) -> None | str | int | float: + def native_value(self) -> None | str | int | float: """Return the state of the entity.""" return self._state_data @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" return self._attribute.unit_of_measurement diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index 634f903c020..48d8443a0a9 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -48,7 +48,7 @@ class WLEDEstimatedCurrentSensor(WLEDEntity, SensorEntity): """Defines a WLED estimated current sensor.""" _attr_icon = "mdi:power" - _attr_unit_of_measurement = ELECTRIC_CURRENT_MILLIAMPERE + _attr_native_unit_of_measurement = ELECTRIC_CURRENT_MILLIAMPERE _attr_device_class = DEVICE_CLASS_CURRENT def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: @@ -66,7 +66,7 @@ class WLEDEstimatedCurrentSensor(WLEDEntity, SensorEntity): } @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" return self.coordinator.data.info.leds.power @@ -84,7 +84,7 @@ class WLEDUptimeSensor(WLEDEntity, SensorEntity): self._attr_unique_id = f"{coordinator.data.info.mac_address}_uptime" @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" uptime = utcnow() - timedelta(seconds=self.coordinator.data.info.uptime) return uptime.replace(microsecond=0).isoformat() @@ -95,7 +95,7 @@ class WLEDFreeHeapSensor(WLEDEntity, SensorEntity): _attr_icon = "mdi:memory" _attr_entity_registry_enabled_default = False - _attr_unit_of_measurement = DATA_BYTES + _attr_native_unit_of_measurement = DATA_BYTES def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED free heap sensor.""" @@ -104,7 +104,7 @@ class WLEDFreeHeapSensor(WLEDEntity, SensorEntity): self._attr_unique_id = f"{coordinator.data.info.mac_address}_free_heap" @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" return self.coordinator.data.info.free_heap @@ -113,7 +113,7 @@ class WLEDWifiSignalSensor(WLEDEntity, SensorEntity): """Defines a WLED Wi-Fi signal sensor.""" _attr_icon = "mdi:wifi" - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE _attr_entity_registry_enabled_default = False def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: @@ -123,7 +123,7 @@ class WLEDWifiSignalSensor(WLEDEntity, SensorEntity): self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_signal" @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of the sensor.""" if not self.coordinator.data.info.wifi: return None @@ -134,7 +134,7 @@ class WLEDWifiRSSISensor(WLEDEntity, SensorEntity): """Defines a WLED Wi-Fi RSSI sensor.""" _attr_device_class = DEVICE_CLASS_SIGNAL_STRENGTH - _attr_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT + _attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT _attr_entity_registry_enabled_default = False def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: @@ -144,7 +144,7 @@ class WLEDWifiRSSISensor(WLEDEntity, SensorEntity): self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_rssi" @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of the sensor.""" if not self.coordinator.data.info.wifi: return None @@ -164,7 +164,7 @@ class WLEDWifiChannelSensor(WLEDEntity, SensorEntity): self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_channel" @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of the sensor.""" if not self.coordinator.data.info.wifi: return None @@ -184,7 +184,7 @@ class WLEDWifiBSSIDSensor(WLEDEntity, SensorEntity): self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_bssid" @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" if not self.coordinator.data.info.wifi: return None diff --git a/homeassistant/components/wolflink/sensor.py b/homeassistant/components/wolflink/sensor.py index 0d35d4bce5c..92f18e04de4 100644 --- a/homeassistant/components/wolflink/sensor.py +++ b/homeassistant/components/wolflink/sensor.py @@ -63,7 +63,7 @@ class WolfLinkSensor(CoordinatorEntity, SensorEntity): return f"{self.wolf_object.name}" @property - def state(self): + def native_value(self): """Return the state. Wolf Client is returning only changed values so we need to store old value here.""" if self.wolf_object.parameter_id in self.coordinator.data: new_state = self.coordinator.data[self.wolf_object.parameter_id] @@ -95,7 +95,7 @@ class WolfLinkHours(WolfLinkSensor): return "mdi:clock" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return TIME_HOURS @@ -109,7 +109,7 @@ class WolfLinkTemperature(WolfLinkSensor): return DEVICE_CLASS_TEMPERATURE @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return TEMP_CELSIUS @@ -123,7 +123,7 @@ class WolfLinkPressure(WolfLinkSensor): return DEVICE_CLASS_PRESSURE @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return PRESSURE_BAR @@ -132,7 +132,7 @@ class WolfLinkPercentage(WolfLinkSensor): """Class for percentage based entities.""" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self.wolf_object.unit @@ -146,7 +146,7 @@ class WolfLinkState(WolfLinkSensor): return "wolflink__state" @property - def state(self): + def native_value(self): """Return the state converting with supported values.""" state = super().state resolved_state = [ diff --git a/homeassistant/components/worldclock/sensor.py b/homeassistant/components/worldclock/sensor.py index de5b3991e3f..74da12f7f61 100644 --- a/homeassistant/components/worldclock/sensor.py +++ b/homeassistant/components/worldclock/sensor.py @@ -54,7 +54,7 @@ class WorldClockSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/worldtidesinfo/sensor.py b/homeassistant/components/worldtidesinfo/sensor.py index 0fa65957e40..4d7a32605b0 100644 --- a/homeassistant/components/worldtidesinfo/sensor.py +++ b/homeassistant/components/worldtidesinfo/sensor.py @@ -88,7 +88,7 @@ class WorldTidesInfoSensor(SensorEntity): return attr @property - def state(self): + def native_value(self): """Return the state of the device.""" if self.data: if "High" in str(self.data["extremes"][0]["type"]): diff --git a/homeassistant/components/worxlandroid/sensor.py b/homeassistant/components/worxlandroid/sensor.py index e7600670c52..b34481d0990 100644 --- a/homeassistant/components/worxlandroid/sensor.py +++ b/homeassistant/components/worxlandroid/sensor.py @@ -68,12 +68,12 @@ class WorxLandroidSensor(SensorEntity): return f"worxlandroid-{self.sensor}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of the sensor.""" if self.sensor == "battery": return PERCENTAGE diff --git a/homeassistant/components/wsdot/sensor.py b/homeassistant/components/wsdot/sensor.py index 153d496a7d6..bc0023ac54f 100644 --- a/homeassistant/components/wsdot/sensor.py +++ b/homeassistant/components/wsdot/sensor.py @@ -88,7 +88,7 @@ class WashingtonStateTransportSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -96,7 +96,7 @@ class WashingtonStateTransportSensor(SensorEntity): class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): """Travel time sensor from WSDOT.""" - _attr_unit_of_measurement = TIME_MINUTES + _attr_native_unit_of_measurement = TIME_MINUTES def __init__(self, name, access_code, travel_time_id): """Construct a travel time sensor.""" diff --git a/homeassistant/components/xbee/__init__.py b/homeassistant/components/xbee/__init__.py index 13cd4217b4d..5ca9e4ef6f7 100644 --- a/homeassistant/components/xbee/__init__.py +++ b/homeassistant/components/xbee/__init__.py @@ -369,7 +369,7 @@ class XBeeDigitalOut(XBeeDigitalIn): class XBeeAnalogIn(SensorEntity): """Representation of a GPIO pin configured as an analog input.""" - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, config, device): """Initialize the XBee analog in device.""" @@ -416,7 +416,7 @@ class XBeeAnalogIn(SensorEntity): return self._config.should_poll @property - def state(self): + def sensor_state(self): """Return the state of the entity.""" return self._value diff --git a/homeassistant/components/xbee/sensor.py b/homeassistant/components/xbee/sensor.py index b1d5ece7d57..8dae25ad5e1 100644 --- a/homeassistant/components/xbee/sensor.py +++ b/homeassistant/components/xbee/sensor.py @@ -47,7 +47,7 @@ class XBeeTemperatureSensor(SensorEntity): """Representation of XBee Pro temperature sensor.""" _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_unit_of_measurement = TEMP_CELSIUS + _attr_native_unit_of_measurement = TEMP_CELSIUS def __init__(self, config, device): """Initialize the sensor.""" @@ -61,7 +61,7 @@ class XBeeTemperatureSensor(SensorEntity): return self._config.name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._temp diff --git a/homeassistant/components/xbox/sensor.py b/homeassistant/components/xbox/sensor.py index 9aa0de4a727..854c0b007f6 100644 --- a/homeassistant/components/xbox/sensor.py +++ b/homeassistant/components/xbox/sensor.py @@ -33,7 +33,7 @@ class XboxSensorEntity(XboxBaseSensorEntity, SensorEntity): """Representation of a Xbox presence state.""" @property - def state(self): + def native_value(self): """Return the state of the requested attribute.""" if not self.coordinator.last_update_success: return None diff --git a/homeassistant/components/xbox_live/sensor.py b/homeassistant/components/xbox_live/sensor.py index 2717bc1ad62..c09b707cba0 100644 --- a/homeassistant/components/xbox_live/sensor.py +++ b/homeassistant/components/xbox_live/sensor.py @@ -98,7 +98,7 @@ class XboxSensor(SensorEntity): return False @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/xiaomi_aqara/sensor.py b/homeassistant/components/xiaomi_aqara/sensor.py index cc49bb14251..cad3afb11ba 100644 --- a/homeassistant/components/xiaomi_aqara/sensor.py +++ b/homeassistant/components/xiaomi_aqara/sensor.py @@ -125,7 +125,7 @@ class XiaomiSensor(XiaomiDevice, SensorEntity): return None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" try: return SENSOR_TYPES.get(self._data_key)[0] @@ -142,7 +142,7 @@ class XiaomiSensor(XiaomiDevice, SensorEntity): ) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -176,7 +176,7 @@ class XiaomiBatterySensor(XiaomiDevice, SensorEntity): """Representation of a XiaomiSensor.""" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return PERCENTAGE @@ -186,7 +186,7 @@ class XiaomiBatterySensor(XiaomiDevice, SensorEntity): return DEVICE_CLASS_BATTERY @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index c180bb75a77..bbf83825dca 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -100,34 +100,34 @@ SENSOR_TYPES = { ATTR_TEMPERATURE: XiaomiMiioSensorDescription( key=ATTR_TEMPERATURE, name="Temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), ATTR_HUMIDITY: XiaomiMiioSensorDescription( key=ATTR_HUMIDITY, name="Humidity", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), ATTR_PRESSURE: XiaomiMiioSensorDescription( key=ATTR_PRESSURE, name="Pressure", - unit_of_measurement=PRESSURE_HPA, + native_unit_of_measurement=PRESSURE_HPA, device_class=DEVICE_CLASS_PRESSURE, state_class=STATE_CLASS_MEASUREMENT, ), ATTR_LOAD_POWER: XiaomiMiioSensorDescription( key=ATTR_LOAD_POWER, name="Load Power", - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, ), ATTR_WATER_LEVEL: XiaomiMiioSensorDescription( key=ATTR_WATER_LEVEL, name="Water Level", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:water-check", state_class=STATE_CLASS_MEASUREMENT, valid_min_value=0.0, @@ -136,27 +136,27 @@ SENSOR_TYPES = { ATTR_ACTUAL_SPEED: XiaomiMiioSensorDescription( key=ATTR_ACTUAL_SPEED, name="Actual Speed", - unit_of_measurement="rpm", + native_unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=STATE_CLASS_MEASUREMENT, ), ATTR_MOTOR_SPEED: XiaomiMiioSensorDescription( key=ATTR_MOTOR_SPEED, name="Motor Speed", - unit_of_measurement="rpm", + native_unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=STATE_CLASS_MEASUREMENT, ), ATTR_ILLUMINANCE: XiaomiMiioSensorDescription( key=ATTR_ILLUMINANCE, name="Illuminance", - unit_of_measurement=UNIT_LUMEN, + native_unit_of_measurement=UNIT_LUMEN, device_class=DEVICE_CLASS_ILLUMINANCE, state_class=STATE_CLASS_MEASUREMENT, ), ATTR_AIR_QUALITY: XiaomiMiioSensorDescription( key=ATTR_AIR_QUALITY, - unit_of_measurement="AQI", + native_unit_of_measurement="AQI", icon="mdi:cloud", state_class=STATE_CLASS_MEASUREMENT, ), @@ -331,7 +331,7 @@ class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): return self._available @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state @@ -378,7 +378,7 @@ class XiaomiGatewaySensor(XiaomiGatewayDevice, SensorEntity): self.entity_description = description @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._sub_device.status[self.entity_description.key] @@ -403,7 +403,7 @@ class XiaomiGatewayIlluminanceSensor(SensorEntity): return self._available @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/xs1/sensor.py b/homeassistant/components/xs1/sensor.py index f158e7d74b8..ed022f5b9e7 100644 --- a/homeassistant/components/xs1/sensor.py +++ b/homeassistant/components/xs1/sensor.py @@ -37,11 +37,11 @@ class XS1Sensor(XS1DeviceEntity, SensorEntity): return self.device.name() @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self.device.value() @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self.device.unit() diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index 08e856a721e..b4f7f986626 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -108,7 +108,7 @@ class DiscoverYandexTransport(SensorEntity): self._attrs = attrs @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/zabbix/sensor.py b/homeassistant/components/zabbix/sensor.py index a2644287690..ff2e2c4d9ba 100644 --- a/homeassistant/components/zabbix/sensor.py +++ b/homeassistant/components/zabbix/sensor.py @@ -94,12 +94,12 @@ class ZabbixTriggerCountSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the units of measurement.""" return "issues" diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index a6018de831e..5659e4835db 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -171,12 +171,12 @@ class ZamgSensor(SensorEntity): return f"{self.client_name} {self.variable}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self.probe.get_data(self.variable) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return SENSOR_TYPES[self.variable][1] diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index 0333bb76a20..bac32563776 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -77,7 +77,7 @@ class ZestimateDataSensor(SensorEntity): return f"{self._name} {self.address}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" try: return round(float(self._state), 1) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 3c3aba919ed..cc401cb1e05 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -135,12 +135,12 @@ class Sensor(ZhaEntity, SensorEntity): return self._state_class @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity.""" return self._unit @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the entity.""" assert self.SENSOR_ATTR is not None raw_state = self._channel.cluster.get(self.SENSOR_ATTR) @@ -274,7 +274,7 @@ class SmartEnergyMetering(Sensor): return self._channel.formatter_function(value) @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return Unit of measurement.""" return self._channel.unit_of_measurement diff --git a/homeassistant/components/zodiac/sensor.py b/homeassistant/components/zodiac/sensor.py index 80a4f782915..b337dda1db0 100644 --- a/homeassistant/components/zodiac/sensor.py +++ b/homeassistant/components/zodiac/sensor.py @@ -196,7 +196,7 @@ class ZodiacSensor(SensorEntity): return "zodiac__sign" @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the device.""" return self._state diff --git a/homeassistant/components/zoneminder/sensor.py b/homeassistant/components/zoneminder/sensor.py index 701f4b490d3..d392901b633 100644 --- a/homeassistant/components/zoneminder/sensor.py +++ b/homeassistant/components/zoneminder/sensor.py @@ -71,7 +71,7 @@ class ZMSensorMonitors(SensorEntity): return f"{self._monitor.name} Status" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -107,12 +107,12 @@ class ZMSensorEvents(SensorEntity): return f"{self._monitor.name} {self.time_period.title}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return "Events" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -136,7 +136,7 @@ class ZMSensorRunState(SensorEntity): return "Run State" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/zwave/sensor.py b/homeassistant/components/zwave/sensor.py index d973e52ff92..75046c2f9d8 100644 --- a/homeassistant/components/zwave/sensor.py +++ b/homeassistant/components/zwave/sensor.py @@ -56,12 +56,12 @@ class ZWaveSensor(ZWaveDeviceEntity, SensorEntity): return True @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement the value is expressed in.""" return self._units @@ -70,7 +70,7 @@ class ZWaveMultilevelSensor(ZWaveSensor): """Representation of a multi level sensor Z-Wave sensor.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._units in ("C", "F"): return round(self._state, 1) @@ -87,7 +87,7 @@ class ZWaveMultilevelSensor(ZWaveSensor): return None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" if self._units == "C": return TEMP_CELSIUS diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 7b491661e68..aa163fa8bd9 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -181,14 +181,14 @@ class ZWaveStringSensor(ZwaveSensorBase): """Representation of a Z-Wave String sensor.""" @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return state of the sensor.""" if self.info.primary_value.value is None: return None return str(self.info.primary_value.value) @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return unit of measurement the value is expressed in.""" if self.info.primary_value.metadata.unit is None: return None @@ -215,14 +215,14 @@ class ZWaveNumericSensor(ZwaveSensorBase): ) @property - def state(self) -> float: + def native_value(self) -> float: """Return state of the sensor.""" if self.info.primary_value.value is None: return 0 return round(float(self.info.primary_value.value), 2) @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return unit of measurement the value is expressed in.""" if self.info.primary_value.metadata.unit is None: return None @@ -345,7 +345,7 @@ class ZWaveListSensor(ZwaveSensorBase): ) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return state of the sensor.""" if self.info.primary_value.value is None: return None @@ -387,7 +387,7 @@ class ZWaveConfigParameterSensor(ZwaveSensorBase): ) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return state of the sensor.""" if self.info.primary_value.value is None: return None @@ -439,7 +439,7 @@ class ZWaveNodeStatusSensor(SensorEntity): self._attr_device_info = { "identifiers": {get_device_id(self.client, self.node)}, } - self._attr_state: str = node.status.name.lower() + self._attr_native_value: str = node.status.name.lower() async def async_poll_value(self, _: bool) -> None: """Poll a value.""" @@ -447,7 +447,7 @@ class ZWaveNodeStatusSensor(SensorEntity): def _status_changed(self, _: dict) -> None: """Call when status event is received.""" - self._attr_state = self.node.status.name.lower() + self._attr_native_value = self.node.status.name.lower() self.async_write_ha_state() async def async_added_to_hass(self) -> None: diff --git a/tests/components/vultr/test_sensor.py b/tests/components/vultr/test_sensor.py index 4449859ddb2..e1dbda1dd04 100644 --- a/tests/components/vultr/test_sensor.py +++ b/tests/components/vultr/test_sensor.py @@ -29,6 +29,7 @@ class TestVultrSensorSetup(unittest.TestCase): def add_entities(self, devices, action): """Mock add devices.""" for device in devices: + device.hass = self.hass self.DEVICES.append(device) def setUp(self): diff --git a/tests/components/wsdot/test_sensor.py b/tests/components/wsdot/test_sensor.py index bbb56efdeda..f1c96bc3ed8 100644 --- a/tests/components/wsdot/test_sensor.py +++ b/tests/components/wsdot/test_sensor.py @@ -35,6 +35,9 @@ async def test_setup(hass, requests_mock): def add_entities(new_entities, update_before_add=False): """Mock add entities.""" + for entity in new_entities: + entity.hass = hass + if update_before_add: for entity in new_entities: entity.update() diff --git a/tests/components/zwave/test_sensor.py b/tests/components/zwave/test_sensor.py index ae0fa44ed8c..4f995131d15 100644 --- a/tests/components/zwave/test_sensor.py +++ b/tests/components/zwave/test_sensor.py @@ -70,8 +70,10 @@ def test_get_device_detects_battery_sensor(mock_openzwave): assert device.device_class == homeassistant.const.DEVICE_CLASS_BATTERY -def test_multilevelsensor_value_changed_temp_fahrenheit(mock_openzwave): +def test_multilevelsensor_value_changed_temp_fahrenheit(hass, mock_openzwave): """Test value changed for Z-Wave multilevel sensor for temperature.""" + hass.config.units.temperature_unit = homeassistant.const.TEMP_FAHRENHEIT + node = MockNode( command_classes=[ const.COMMAND_CLASS_SENSOR_MULTILEVEL, @@ -82,6 +84,7 @@ def test_multilevelsensor_value_changed_temp_fahrenheit(mock_openzwave): values = MockEntityValues(primary=value) device = sensor.get_device(node=node, values=values, node_config={}) + 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 @@ -90,8 +93,9 @@ def test_multilevelsensor_value_changed_temp_fahrenheit(mock_openzwave): assert device.state == 198.0 -def test_multilevelsensor_value_changed_temp_celsius(mock_openzwave): +def test_multilevelsensor_value_changed_temp_celsius(hass, mock_openzwave): """Test value changed for Z-Wave multilevel sensor for temperature.""" + hass.config.units.temperature_unit = homeassistant.const.TEMP_CELSIUS node = MockNode( command_classes=[ const.COMMAND_CLASS_SENSOR_MULTILEVEL, @@ -102,6 +106,7 @@ def test_multilevelsensor_value_changed_temp_celsius(mock_openzwave): values = MockEntityValues(primary=value) device = sensor.get_device(node=node, values=values, node_config={}) + 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 @@ -110,7 +115,7 @@ def test_multilevelsensor_value_changed_temp_celsius(mock_openzwave): assert device.state == 38.0 -def test_multilevelsensor_value_changed_other_units(mock_openzwave): +def test_multilevelsensor_value_changed_other_units(hass, mock_openzwave): """Test value changed for Z-Wave multilevel sensor for other units.""" node = MockNode( command_classes=[ @@ -124,6 +129,7 @@ def test_multilevelsensor_value_changed_other_units(mock_openzwave): values = MockEntityValues(primary=value) device = sensor.get_device(node=node, values=values, node_config={}) + device.hass = hass assert device.state == 190.96 assert device.unit_of_measurement == homeassistant.const.ENERGY_KILO_WATT_HOUR assert device.device_class is None @@ -132,7 +138,7 @@ def test_multilevelsensor_value_changed_other_units(mock_openzwave): assert device.state == 197.96 -def test_multilevelsensor_value_changed_integer(mock_openzwave): +def test_multilevelsensor_value_changed_integer(hass, mock_openzwave): """Test value changed for Z-Wave multilevel sensor for other units.""" node = MockNode( command_classes=[ @@ -144,6 +150,7 @@ def test_multilevelsensor_value_changed_integer(mock_openzwave): values = MockEntityValues(primary=value) device = sensor.get_device(node=node, values=values, node_config={}) + device.hass = hass assert device.state == 5 assert device.unit_of_measurement == "counts" assert device.device_class is None @@ -152,7 +159,7 @@ def test_multilevelsensor_value_changed_integer(mock_openzwave): assert device.state == 6 -def test_alarm_sensor_value_changed(mock_openzwave): +def test_alarm_sensor_value_changed(hass, mock_openzwave): """Test value changed for Z-Wave sensor.""" node = MockNode( command_classes=[const.COMMAND_CLASS_ALARM, const.COMMAND_CLASS_SENSOR_ALARM] @@ -161,6 +168,7 @@ def test_alarm_sensor_value_changed(mock_openzwave): values = MockEntityValues(primary=value) device = sensor.get_device(node=node, values=values, node_config={}) + device.hass = hass assert device.state == 12.34 assert device.unit_of_measurement == homeassistant.const.PERCENTAGE assert device.device_class is None From 2720ba275377f7949538d7a8c1254faea34af63e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Aug 2021 21:17:47 +0200 Subject: [PATCH 165/355] Move temperature conversions to sensor base class (4/8) (#54472) * Move temperature conversions to entity base class (4/8) * Fix litterrobot sensor * Fix tests --- homeassistant/components/iammeter/sensor.py | 4 +-- homeassistant/components/iaqualink/sensor.py | 4 +-- homeassistant/components/icloud/sensor.py | 4 +-- homeassistant/components/ihc/sensor.py | 4 +-- homeassistant/components/imap/sensor.py | 2 +- .../components/imap_email_content/sensor.py | 2 +- homeassistant/components/incomfort/sensor.py | 4 +-- homeassistant/components/influxdb/sensor.py | 4 +-- .../components/integration/sensor.py | 4 +-- homeassistant/components/ios/sensor.py | 10 +++++-- homeassistant/components/iota/sensor.py | 6 ++-- homeassistant/components/iperf3/sensor.py | 4 +-- homeassistant/components/ipp/sensor.py | 8 +++--- homeassistant/components/iqvia/__init__.py | 2 +- homeassistant/components/iqvia/sensor.py | 4 +-- .../components/irish_rail_transport/sensor.py | 4 +-- .../components/islamic_prayer_times/sensor.py | 2 +- homeassistant/components/isy994/sensor.py | 6 ++-- .../components/jewish_calendar/sensor.py | 4 +-- homeassistant/components/juicenet/sensor.py | 14 +++++----- homeassistant/components/kaiterra/sensor.py | 4 +-- homeassistant/components/keba/sensor.py | 4 +-- homeassistant/components/kira/sensor.py | 2 +- homeassistant/components/knx/sensor.py | 4 +-- homeassistant/components/konnected/sensor.py | 4 +-- .../components/kostal_plenticore/sensor.py | 4 +-- homeassistant/components/kraken/sensor.py | 4 +-- homeassistant/components/kwb/sensor.py | 4 +-- homeassistant/components/lacrosse/sensor.py | 10 +++---- homeassistant/components/lastfm/sensor.py | 2 +- .../components/launch_library/sensor.py | 2 +- homeassistant/components/lcn/sensor.py | 6 ++-- homeassistant/components/lightwave/sensor.py | 4 +-- .../components/linux_battery/sensor.py | 4 +-- .../components/litterrobot/sensor.py | 8 +++--- homeassistant/components/local_ip/sensor.py | 2 +- .../components/logi_circle/sensor.py | 4 +-- homeassistant/components/london_air/sensor.py | 2 +- .../components/london_underground/sensor.py | 2 +- homeassistant/components/loopenergy/sensor.py | 4 +-- homeassistant/components/luftdaten/sensor.py | 4 +-- homeassistant/components/lyft/sensor.py | 4 +-- homeassistant/components/lyric/sensor.py | 12 ++++---- .../components/magicseaweed/sensor.py | 4 +-- homeassistant/components/mazda/sensor.py | 28 +++++++++---------- homeassistant/components/melcloud/sensor.py | 18 ++++++------ .../components/meteo_france/sensor.py | 8 +++--- .../components/meteoclimatic/sensor.py | 6 ++-- homeassistant/components/metoffice/sensor.py | 26 ++++++++--------- homeassistant/components/mfi/sensor.py | 4 +-- homeassistant/components/mhz19/sensor.py | 4 +-- homeassistant/components/miflora/sensor.py | 4 +-- homeassistant/components/min_max/sensor.py | 4 +-- .../components/minecraft_server/sensor.py | 4 +-- homeassistant/components/mitemp_bt/sensor.py | 4 +-- homeassistant/components/mobile_app/sensor.py | 4 +-- homeassistant/components/modbus/sensor.py | 6 ++-- .../components/modem_callerid/sensor.py | 2 +- .../components/modern_forms/sensor.py | 4 +-- .../components/mold_indicator/sensor.py | 4 +-- homeassistant/components/moon/sensor.py | 2 +- .../components/motion_blinds/sensor.py | 10 +++---- homeassistant/components/mqtt/sensor.py | 4 +-- homeassistant/components/mqtt_room/sensor.py | 2 +- homeassistant/components/mvglive/sensor.py | 4 +-- homeassistant/components/mychevy/sensor.py | 6 ++-- homeassistant/components/mysensors/sensor.py | 6 ++-- tests/components/litterrobot/test_sensor.py | 1 + tests/components/mfi/test_sensor.py | 4 ++- tests/components/mhz19/test_sensor.py | 9 ++++-- 70 files changed, 195 insertions(+), 183 deletions(-) diff --git a/homeassistant/components/iammeter/sensor.py b/homeassistant/components/iammeter/sensor.py index b1882619fda..de0e76fc3aa 100644 --- a/homeassistant/components/iammeter/sensor.py +++ b/homeassistant/components/iammeter/sensor.py @@ -86,7 +86,7 @@ class IamMeter(CoordinatorEntity, SensorEntity): self.dev_name = dev_name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self.coordinator.data.data[self.sensor_name] @@ -106,6 +106,6 @@ class IamMeter(CoordinatorEntity, SensorEntity): return "mdi:flash" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self.unit diff --git a/homeassistant/components/iaqualink/sensor.py b/homeassistant/components/iaqualink/sensor.py index ae32db9eb9e..61e4560c3be 100644 --- a/homeassistant/components/iaqualink/sensor.py +++ b/homeassistant/components/iaqualink/sensor.py @@ -31,7 +31,7 @@ class HassAqualinkSensor(AqualinkEntity, SensorEntity): return self.dev.label @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the measurement unit for the sensor.""" if self.dev.name.endswith("_temp"): if self.dev.system.temp_unit == "F": @@ -40,7 +40,7 @@ class HassAqualinkSensor(AqualinkEntity, SensorEntity): return None @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" if self.dev.state == "": return None diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index ec55a1fcedd..5469eadc998 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -54,7 +54,7 @@ class IcloudDeviceBatterySensor(SensorEntity): """Representation of a iCloud device battery sensor.""" _attr_device_class = DEVICE_CLASS_BATTERY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, account: IcloudAccount, device: IcloudDevice) -> None: """Initialize the battery sensor.""" @@ -73,7 +73,7 @@ class IcloudDeviceBatterySensor(SensorEntity): return f"{self._device.name} battery state" @property - def state(self) -> int: + def native_value(self) -> int: """Battery state percentage.""" return self._device.battery_level diff --git a/homeassistant/components/ihc/sensor.py b/homeassistant/components/ihc/sensor.py index d1aec781df7..17c17980c95 100644 --- a/homeassistant/components/ihc/sensor.py +++ b/homeassistant/components/ihc/sensor.py @@ -48,12 +48,12 @@ class IHCSensor(IHCDevice, SensorEntity): ) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/imap/sensor.py b/homeassistant/components/imap/sensor.py index 4158d1be801..c3d6b2198ce 100644 --- a/homeassistant/components/imap/sensor.py +++ b/homeassistant/components/imap/sensor.py @@ -95,7 +95,7 @@ class ImapSensor(SensorEntity): return ICON @property - def state(self): + def native_value(self): """Return the number of emails found.""" return self._email_count diff --git a/homeassistant/components/imap_email_content/sensor.py b/homeassistant/components/imap_email_content/sensor.py index cdd47d68d76..87c18a56bbe 100644 --- a/homeassistant/components/imap_email_content/sensor.py +++ b/homeassistant/components/imap_email_content/sensor.py @@ -165,7 +165,7 @@ class EmailContentSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the current email state.""" return self._message diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index a9e1faaba10..9fb99321ff2 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -59,7 +59,7 @@ class IncomfortSensor(IncomfortChild, SensorEntity): self._unit_of_measurement = None @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" return self._heater.status[self._state_attr] @@ -69,7 +69,7 @@ class IncomfortSensor(IncomfortChild, SensorEntity): return self._device_class @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of the sensor.""" return self._unit_of_measurement diff --git a/homeassistant/components/influxdb/sensor.py b/homeassistant/components/influxdb/sensor.py index c2cb5070a4c..bdbfafaf790 100644 --- a/homeassistant/components/influxdb/sensor.py +++ b/homeassistant/components/influxdb/sensor.py @@ -222,12 +222,12 @@ class InfluxSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 9308adc622d..cabcb2fd394 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -213,12 +213,12 @@ class IntegrationSensor(RestoreEntity, SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return round(self._state, self._round_digits) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/ios/sensor.py b/homeassistant/components/ios/sensor.py index d3b006f9078..c3c1ad2b8ce 100644 --- a/homeassistant/components/ios/sensor.py +++ b/homeassistant/components/ios/sensor.py @@ -14,7 +14,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="level", name="Battery Level", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( key="state", @@ -114,12 +114,16 @@ class IOSSensor(SensorEntity): def _update(self, device): """Get the latest state of the sensor.""" self._device = device - self._attr_state = self._device[ios.ATTR_BATTERY][self.entity_description.key] + self._attr_native_value = self._device[ios.ATTR_BATTERY][ + self.entity_description.key + ] self.async_write_ha_state() async def async_added_to_hass(self) -> None: """Added to hass so need to register to dispatch.""" - self._attr_state = self._device[ios.ATTR_BATTERY][self.entity_description.key] + self._attr_native_value = self._device[ios.ATTR_BATTERY][ + self.entity_description.key + ] device_id = self._device[ios.ATTR_DEVICE_ID] self.async_on_remove( async_dispatcher_connect(self.hass, f"{DOMAIN}.{device_id}", self._update) diff --git a/homeassistant/components/iota/sensor.py b/homeassistant/components/iota/sensor.py index 62260be2410..687a4ca35d6 100644 --- a/homeassistant/components/iota/sensor.py +++ b/homeassistant/components/iota/sensor.py @@ -47,12 +47,12 @@ class IotaBalanceSensor(IotaDevice, SensorEntity): return f"{self._name} Balance" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return "IOTA" @@ -81,7 +81,7 @@ class IotaNodeSensor(IotaDevice, SensorEntity): return "IOTA Node" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/iperf3/sensor.py b/homeassistant/components/iperf3/sensor.py index 610ff91250f..07b9cc069e4 100644 --- a/homeassistant/components/iperf3/sensor.py +++ b/homeassistant/components/iperf3/sensor.py @@ -41,12 +41,12 @@ class Iperf3Sensor(RestoreEntity, SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index 5d736c864e1..e7c0d5c38f5 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -72,7 +72,7 @@ class IPPSensor(IPPEntity, SensorEntity): """Initialize IPP sensor.""" self._key = key self._attr_unique_id = f"{unique_id}_{key}" - self._attr_unit_of_measurement = unit_of_measurement + self._attr_native_unit_of_measurement = unit_of_measurement super().__init__( entry_id=entry_id, @@ -123,7 +123,7 @@ class IPPMarkerSensor(IPPSensor): } @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of the sensor.""" level = self.coordinator.data.markers[self.marker_index].level @@ -164,7 +164,7 @@ class IPPPrinterSensor(IPPSensor): } @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" return self.coordinator.data.state.printer_state @@ -189,7 +189,7 @@ class IPPUptimeSensor(IPPSensor): ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" uptime = utcnow() - timedelta(seconds=self.coordinator.data.info.uptime) return uptime.replace(microsecond=0).isoformat() diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index fa783cc9031..37cc7bedb71 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -109,7 +109,7 @@ class IQVIAEntity(CoordinatorEntity, SensorEntity): self._attr_icon = icon self._attr_name = name self._attr_unique_id = f"{entry.data[CONF_ZIP_CODE]}_{sensor_type}" - self._attr_unit_of_measurement = "index" + self._attr_native_unit_of_measurement = "index" self._entry = entry self._type = sensor_type diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index 5762e4a3888..10d33bfb4bf 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -120,7 +120,7 @@ class ForecastSensor(IQVIAEntity): if i["minimum"] <= average <= i["maximum"] ] - self._attr_state = average + self._attr_native_value = average self._attr_extra_state_attributes.update( { ATTR_CITY: data["City"].title(), @@ -213,4 +213,4 @@ class IndexSensor(IQVIAEntity): f"{attrs['Name'].lower()}_index" ] = attrs["Index"] - self._attr_state = period["Index"] + self._attr_native_value = period["Index"] diff --git a/homeassistant/components/irish_rail_transport/sensor.py b/homeassistant/components/irish_rail_transport/sensor.py index b5ba16f8541..9ec28d73836 100644 --- a/homeassistant/components/irish_rail_transport/sensor.py +++ b/homeassistant/components/irish_rail_transport/sensor.py @@ -83,7 +83,7 @@ class IrishRailTransportSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -114,7 +114,7 @@ class IrishRailTransportSensor(SensorEntity): } @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return TIME_MINUTES diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index 2fa563785d2..99cc65bb548 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -43,7 +43,7 @@ class IslamicPrayerTimeSensor(SensorEntity): return self.sensor_type @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return ( self.client.prayer_times_info.get(self.sensor_type) diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index ebf32384d85..f12f3cb6bdd 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -67,7 +67,7 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): return UOM_FRIENDLY_NAME.get(uom) @property - def state(self) -> str: + def native_value(self) -> str: """Get the state of the ISY994 sensor device.""" value = self._node.status if value == ISY_VALUE_UNKNOWN: @@ -97,7 +97,7 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): return value @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Get the Home Assistant unit of measurement for the device.""" raw_units = self.raw_unit_of_measurement # Check if this is a known index pair UOM @@ -117,7 +117,7 @@ class ISYSensorVariableEntity(ISYEntity, SensorEntity): self._name = vname @property - def state(self): + def native_value(self): """Return the state of the variable.""" return convert_isy_value_to_hass(self._node.status, "", self._node.prec) diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 17a61c932a3..e3f51ea5e2c 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -50,7 +50,7 @@ class JewishCalendarSensor(SensorEntity): self._holiday_attrs = {} @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if isinstance(self._state, datetime): return self._state.isoformat() @@ -134,7 +134,7 @@ class JewishCalendarTimeSensor(JewishCalendarSensor): _attr_device_class = DEVICE_CLASS_TIMESTAMP @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._state is None: return None diff --git a/homeassistant/components/juicenet/sensor.py b/homeassistant/components/juicenet/sensor.py index 435508f823d..4eaaba41b55 100644 --- a/homeassistant/components/juicenet/sensor.py +++ b/homeassistant/components/juicenet/sensor.py @@ -31,40 +31,40 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="temperature", name="Temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="voltage", name="Voltage", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, ), SensorEntityDescription( key="amps", name="Amps", - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="watts", name="Watts", - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="charge_time", name="Charge time", - unit_of_measurement=TIME_SECONDS, + native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", ), SensorEntityDescription( key="energy_added", name="Energy added", - unit_of_measurement=ENERGY_WATT_HOUR, + native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), ) @@ -110,6 +110,6 @@ class JuiceNetSensorDevice(JuiceNetDevice, SensorEntity): return icon @property - def state(self): + def native_value(self): """Return the state.""" return getattr(self.device, self.entity_description.key, None) diff --git a/homeassistant/components/kaiterra/sensor.py b/homeassistant/components/kaiterra/sensor.py index 6c82013361a..fbaa730ab9f 100644 --- a/homeassistant/components/kaiterra/sensor.py +++ b/homeassistant/components/kaiterra/sensor.py @@ -70,7 +70,7 @@ class KaiterraSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state.""" return self._sensor.get("value") @@ -80,7 +80,7 @@ class KaiterraSensor(SensorEntity): return f"{self._device_id}_{self._kind}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" if not self._sensor.get("units"): return None diff --git a/homeassistant/components/keba/sensor.py b/homeassistant/components/keba/sensor.py index 2792246d71c..37b42cb3cbe 100644 --- a/homeassistant/components/keba/sensor.py +++ b/homeassistant/components/keba/sensor.py @@ -104,12 +104,12 @@ class KebaSensor(SensorEntity): return self._icon @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Get the unit of measurement.""" return self._unit diff --git a/homeassistant/components/kira/sensor.py b/homeassistant/components/kira/sensor.py index a6b1b9ada22..b28aac740f1 100644 --- a/homeassistant/components/kira/sensor.py +++ b/homeassistant/components/kira/sensor.py @@ -50,7 +50,7 @@ class KiraReceiver(SensorEntity): return ICON @property - def state(self): + def native_value(self): """Return the state of the receiver.""" return self._state diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index 5fee8446e91..933ba7bf30d 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -62,11 +62,11 @@ class KNXSensor(KnxEntity, SensorEntity): ) self._attr_force_update = self._device.always_callback self._attr_unique_id = str(self._device.sensor_value.group_address_state) - self._attr_unit_of_measurement = self._device.unit_of_measurement() + self._attr_native_unit_of_measurement = self._device.unit_of_measurement() self._attr_state_class = config.get(SensorSchema.CONF_STATE_CLASS) @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the sensor.""" return self._device.resolve_state() diff --git a/homeassistant/components/konnected/sensor.py b/homeassistant/components/konnected/sensor.py index 18975bdb467..a22b30f6862 100644 --- a/homeassistant/components/konnected/sensor.py +++ b/homeassistant/components/konnected/sensor.py @@ -104,12 +104,12 @@ class KonnectedSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py index 099d359e619..57b37e51d11 100644 --- a/homeassistant/components/kostal_plenticore/sensor.py +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -169,7 +169,7 @@ class PlenticoreDataSensor(CoordinatorEntity, SensorEntity): return f"{self.platform_name} {self._sensor_name}" @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of this Sensor Entity or None.""" return self._sensor_data.get(ATTR_UNIT_OF_MEASUREMENT) @@ -199,7 +199,7 @@ class PlenticoreDataSensor(CoordinatorEntity, SensorEntity): return self._sensor_data.get(ATTR_LAST_RESET) @property - def state(self) -> Any | None: + def native_value(self) -> Any | None: """Return the state of the sensor.""" if self.coordinator.data is None: # None is translated to STATE_UNKNOWN diff --git a/homeassistant/components/kraken/sensor.py b/homeassistant/components/kraken/sensor.py index bc0a0a21845..1b9f8ca13cc 100644 --- a/homeassistant/components/kraken/sensor.py +++ b/homeassistant/components/kraken/sensor.py @@ -124,7 +124,7 @@ class KrakenSensor(CoordinatorEntity, SensorEntity): return self._name.lower() @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state.""" return self._state @@ -229,7 +229,7 @@ class KrakenSensor(CoordinatorEntity, SensorEntity): return "mdi:cash" @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" if "number_of" not in self._sensor_type: return self._unit_of_measurement diff --git a/homeassistant/components/kwb/sensor.py b/homeassistant/components/kwb/sensor.py index 1b56803fae6..c6d2794a06d 100644 --- a/homeassistant/components/kwb/sensor.py +++ b/homeassistant/components/kwb/sensor.py @@ -94,13 +94,13 @@ class KWBSensor(SensorEntity): return self._sensor.available @property - def state(self): + def native_value(self): """Return the state of value.""" if self._sensor.value is not None and self._sensor.available: return self._sensor.value return None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._sensor.unit_of_measurement diff --git a/homeassistant/components/lacrosse/sensor.py b/homeassistant/components/lacrosse/sensor.py index 2f93196a4bb..99aa39ce7cd 100644 --- a/homeassistant/components/lacrosse/sensor.py +++ b/homeassistant/components/lacrosse/sensor.py @@ -176,10 +176,10 @@ class LaCrosseTemperature(LaCrosseSensor): """Implementation of a Lacrosse temperature sensor.""" _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_unit_of_measurement = TEMP_CELSIUS + _attr_native_unit_of_measurement = TEMP_CELSIUS @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._temperature @@ -187,11 +187,11 @@ class LaCrosseTemperature(LaCrosseSensor): class LaCrosseHumidity(LaCrosseSensor): """Implementation of a Lacrosse humidity sensor.""" - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE _attr_icon = "mdi:water-percent" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._humidity @@ -200,7 +200,7 @@ class LaCrosseBattery(LaCrosseSensor): """Implementation of a Lacrosse battery sensor.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._low_battery is None: return None diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index 128450826d6..66f05c5d34d 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -77,7 +77,7 @@ class LastfmSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/launch_library/sensor.py b/homeassistant/components/launch_library/sensor.py index 68d2a024bca..18a947f7757 100644 --- a/homeassistant/components/launch_library/sensor.py +++ b/homeassistant/components/launch_library/sensor.py @@ -59,7 +59,7 @@ class LaunchLibrarySensor(SensorEntity): else: if next_launch := next((launch for launch in launches), None): self._attr_available = True - self._attr_state = next_launch.name + self._attr_native_value = next_launch.name self._attr_extra_state_attributes = { ATTR_LAUNCH_TIME: next_launch.net, ATTR_AGENCY: next_launch.launch_service_provider.name, diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index fdd6ee51872..965e9626f66 100644 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -93,12 +93,12 @@ class LcnVariableSensor(LcnEntity, SensorEntity): await self.device_connection.cancel_status_request_handler(self.variable) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the entity.""" return self._value @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" return cast(str, self.unit.value) @@ -145,7 +145,7 @@ class LcnLedLogicSensor(LcnEntity, SensorEntity): await self.device_connection.cancel_status_request_handler(self.source) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the entity.""" return self._value diff --git a/homeassistant/components/lightwave/sensor.py b/homeassistant/components/lightwave/sensor.py index b298b78c7f6..369256ce403 100644 --- a/homeassistant/components/lightwave/sensor.py +++ b/homeassistant/components/lightwave/sensor.py @@ -26,7 +26,7 @@ class LightwaveBattery(SensorEntity): """Lightwave TRV Battery.""" _attr_device_class = DEVICE_CLASS_BATTERY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE _attr_state_class = STATE_CLASS_MEASUREMENT def __init__(self, name, lwlink, serial): @@ -43,7 +43,7 @@ class LightwaveBattery(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/linux_battery/sensor.py b/homeassistant/components/linux_battery/sensor.py index b7746392cee..18f1c81e368 100644 --- a/homeassistant/components/linux_battery/sensor.py +++ b/homeassistant/components/linux_battery/sensor.py @@ -90,12 +90,12 @@ class LinuxBatterySensor(SensorEntity): return DEVICE_CLASS_BATTERY @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._battery_stat.capacity @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return PERCENTAGE diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 15ea68f8342..cbcb75c0b23 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -36,7 +36,7 @@ class LitterRobotPropertySensor(LitterRobotEntity, SensorEntity): self.sensor_attribute = sensor_attribute @property - def state(self) -> str: + def native_value(self) -> str: """Return the state.""" return getattr(self.robot, self.sensor_attribute) @@ -45,7 +45,7 @@ class LitterRobotWasteSensor(LitterRobotPropertySensor): """Litter-Robot waste sensor.""" @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return unit of measurement.""" return PERCENTAGE @@ -59,10 +59,10 @@ class LitterRobotSleepTimeSensor(LitterRobotPropertySensor): """Litter-Robot sleep time sensor.""" @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state.""" if self.robot.sleep_mode_enabled: - return super().state.isoformat() + return super().native_value.isoformat() return None @property diff --git a/homeassistant/components/local_ip/sensor.py b/homeassistant/components/local_ip/sensor.py index 661ef88e641..bd1b3d54fac 100644 --- a/homeassistant/components/local_ip/sensor.py +++ b/homeassistant/components/local_ip/sensor.py @@ -33,6 +33,6 @@ class IPSensor(SensorEntity): async def async_update(self) -> None: """Fetch new state data for the sensor.""" - self._attr_state = await async_get_source_ip( + self._attr_native_value = await async_get_source_ip( self.hass, target_ip=PUBLIC_TARGET_IP ) diff --git a/homeassistant/components/logi_circle/sensor.py b/homeassistant/components/logi_circle/sensor.py index 29cd6e28e1c..a4158762b37 100644 --- a/homeassistant/components/logi_circle/sensor.py +++ b/homeassistant/components/logi_circle/sensor.py @@ -67,7 +67,7 @@ class LogiSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -112,7 +112,7 @@ class LogiSensor(SensorEntity): return self._icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the units of measurement.""" return SENSOR_TYPES.get(self._sensor_type)[1] diff --git a/homeassistant/components/london_air/sensor.py b/homeassistant/components/london_air/sensor.py index 23eea5c00e0..23bc67f46bc 100644 --- a/homeassistant/components/london_air/sensor.py +++ b/homeassistant/components/london_air/sensor.py @@ -108,7 +108,7 @@ class AirSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/london_underground/sensor.py b/homeassistant/components/london_underground/sensor.py index b7f2cb50cbf..eb962772fe5 100644 --- a/homeassistant/components/london_underground/sensor.py +++ b/homeassistant/components/london_underground/sensor.py @@ -67,7 +67,7 @@ class LondonTubeSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/loopenergy/sensor.py b/homeassistant/components/loopenergy/sensor.py index 78e55f22eb8..05d7f79ebfd 100644 --- a/homeassistant/components/loopenergy/sensor.py +++ b/homeassistant/components/loopenergy/sensor.py @@ -97,7 +97,7 @@ class LoopEnergySensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -107,7 +107,7 @@ class LoopEnergySensor(SensorEntity): return False @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/luftdaten/sensor.py b/homeassistant/components/luftdaten/sensor.py index b27cc35ab26..b4bdd7f30b3 100644 --- a/homeassistant/components/luftdaten/sensor.py +++ b/homeassistant/components/luftdaten/sensor.py @@ -73,7 +73,7 @@ class LuftdatenSensor(SensorEntity): return self._icon @property - def state(self): + def native_value(self): """Return the state of the device.""" if self._data is not None: try: @@ -82,7 +82,7 @@ class LuftdatenSensor(SensorEntity): return None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/lyft/sensor.py b/homeassistant/components/lyft/sensor.py index 39cfff38a1b..84e3744a0e2 100644 --- a/homeassistant/components/lyft/sensor.py +++ b/homeassistant/components/lyft/sensor.py @@ -103,12 +103,12 @@ class LyftSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index f4d4d4b999a..868b6262ddc 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -94,7 +94,7 @@ class LyricSensor(LyricDeviceEntity, SensorEntity): return self._device_class @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return self._unit_of_measurement @@ -123,7 +123,7 @@ class LyricIndoorTemperatureSensor(LyricSensor): ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" return self.device.indoorTemperature @@ -152,7 +152,7 @@ class LyricOutdoorTemperatureSensor(LyricSensor): ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" return self.device.outdoorTemperature @@ -181,7 +181,7 @@ class LyricOutdoorHumiditySensor(LyricSensor): ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" return self.device.displayedOutdoorHumidity @@ -209,7 +209,7 @@ class LyricNextPeriodSensor(LyricSensor): ) @property - def state(self) -> datetime: + def native_value(self) -> datetime: """Return the state of the sensor.""" device = self.device time = dt_util.parse_time(device.changeableValues.nextPeriodTime) @@ -242,7 +242,7 @@ class LyricSetpointStatusSensor(LyricSensor): ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" device = self.device if device.changeableValues.thermostatSetpointStatus == PRESET_HOLD_UNTIL: diff --git a/homeassistant/components/magicseaweed/sensor.py b/homeassistant/components/magicseaweed/sensor.py index 0dd27a60ae0..12288c5ab78 100644 --- a/homeassistant/components/magicseaweed/sensor.py +++ b/homeassistant/components/magicseaweed/sensor.py @@ -115,7 +115,7 @@ class MagicSeaweedSensor(SensorEntity): return f"{self.hour} {self.client_name} {self._name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -125,7 +125,7 @@ class MagicSeaweedSensor(SensorEntity): return self._unit_system @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/mazda/sensor.py b/homeassistant/components/mazda/sensor.py index 673c965544b..03bfbd23b31 100644 --- a/homeassistant/components/mazda/sensor.py +++ b/homeassistant/components/mazda/sensor.py @@ -46,7 +46,7 @@ class MazdaFuelRemainingSensor(MazdaEntity, SensorEntity): return f"{self.vin}_fuel_remaining_percentage" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return PERCENTAGE @@ -56,7 +56,7 @@ class MazdaFuelRemainingSensor(MazdaEntity, SensorEntity): return "mdi:gas-station" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self.data["status"]["fuelRemainingPercent"] @@ -76,7 +76,7 @@ class MazdaFuelDistanceSensor(MazdaEntity, SensorEntity): return f"{self.vin}_fuel_distance_remaining" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: return LENGTH_MILES @@ -88,7 +88,7 @@ class MazdaFuelDistanceSensor(MazdaEntity, SensorEntity): return "mdi:gas-station" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" fuel_distance_km = self.data["status"]["fuelDistanceRemainingKm"] return ( @@ -115,7 +115,7 @@ class MazdaOdometerSensor(MazdaEntity, SensorEntity): return f"{self.vin}_odometer" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: return LENGTH_MILES @@ -127,7 +127,7 @@ class MazdaOdometerSensor(MazdaEntity, SensorEntity): return "mdi:speedometer" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" odometer_km = self.data["status"]["odometerKm"] return ( @@ -152,7 +152,7 @@ class MazdaFrontLeftTirePressureSensor(MazdaEntity, SensorEntity): return f"{self.vin}_front_left_tire_pressure" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return PRESSURE_PSI @@ -162,7 +162,7 @@ class MazdaFrontLeftTirePressureSensor(MazdaEntity, SensorEntity): return "mdi:car-tire-alert" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" tire_pressure = self.data["status"]["tirePressure"]["frontLeftTirePressurePsi"] return None if tire_pressure is None else round(tire_pressure) @@ -183,7 +183,7 @@ class MazdaFrontRightTirePressureSensor(MazdaEntity, SensorEntity): return f"{self.vin}_front_right_tire_pressure" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return PRESSURE_PSI @@ -193,7 +193,7 @@ class MazdaFrontRightTirePressureSensor(MazdaEntity, SensorEntity): return "mdi:car-tire-alert" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" tire_pressure = self.data["status"]["tirePressure"]["frontRightTirePressurePsi"] return None if tire_pressure is None else round(tire_pressure) @@ -214,7 +214,7 @@ class MazdaRearLeftTirePressureSensor(MazdaEntity, SensorEntity): return f"{self.vin}_rear_left_tire_pressure" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return PRESSURE_PSI @@ -224,7 +224,7 @@ class MazdaRearLeftTirePressureSensor(MazdaEntity, SensorEntity): return "mdi:car-tire-alert" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" tire_pressure = self.data["status"]["tirePressure"]["rearLeftTirePressurePsi"] return None if tire_pressure is None else round(tire_pressure) @@ -245,7 +245,7 @@ class MazdaRearRightTirePressureSensor(MazdaEntity, SensorEntity): return f"{self.vin}_rear_right_tire_pressure" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return PRESSURE_PSI @@ -255,7 +255,7 @@ class MazdaRearRightTirePressureSensor(MazdaEntity, SensorEntity): return "mdi:car-tire-alert" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" tire_pressure = self.data["status"]["tirePressure"]["rearRightTirePressurePsi"] return None if tire_pressure is None else round(tire_pressure) diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index 6c303e8e3c3..12029127b84 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -41,7 +41,7 @@ ATA_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( key="room_temperature", name="Room Temperature", icon="mdi:thermometer", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, value_fn=lambda x: x.device.room_temperature, enabled=lambda x: True, @@ -50,7 +50,7 @@ ATA_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( key="energy", name="Energy", icon="mdi:factory", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, value_fn=lambda x: x.device.total_energy_consumed, enabled=lambda x: x.device.has_energy_consumed_meter, @@ -61,7 +61,7 @@ ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( key="outside_temperature", name="Outside Temperature", icon="mdi:thermometer", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, value_fn=lambda x: x.device.outside_temperature, enabled=lambda x: True, @@ -70,7 +70,7 @@ ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( key="tank_temperature", name="Tank Temperature", icon="mdi:thermometer", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, value_fn=lambda x: x.device.tank_temperature, enabled=lambda x: True, @@ -81,7 +81,7 @@ ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( key="room_temperature", name="Room Temperature", icon="mdi:thermometer", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, value_fn=lambda zone: zone.room_temperature, enabled=lambda x: True, @@ -90,7 +90,7 @@ ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( key="flow_temperature", name="Flow Temperature", icon="mdi:thermometer", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, value_fn=lambda zone: zone.flow_temperature, enabled=lambda x: True, @@ -99,7 +99,7 @@ ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( key="return_temperature", name="Flow Return Temperature", icon="mdi:thermometer", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, value_fn=lambda zone: zone.return_temperature, enabled=lambda x: True, @@ -156,7 +156,7 @@ class MelDeviceSensor(SensorEntity): self._attr_last_reset = dt_util.utc_from_timestamp(0) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self.entity_description.value_fn(self._api) @@ -187,6 +187,6 @@ class AtwZoneSensor(MelDeviceSensor): self._attr_name = f"{api.name} {zone.name} {description.name}" @property - def state(self): + def native_value(self): """Return zone based state.""" return self.entity_description.value_fn(self._zone) diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index ed1978d160d..df006c78194 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -109,7 +109,7 @@ class MeteoFranceSensor(CoordinatorEntity, SensorEntity): } @property - def state(self): + def native_value(self): """Return the state.""" path = SENSOR_TYPES[self._type][ENTITY_API_DATA_PATH].split(":") data = getattr(self.coordinator.data, path[0]) @@ -135,7 +135,7 @@ class MeteoFranceSensor(CoordinatorEntity, SensorEntity): return value @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return SENSOR_TYPES[self._type][ENTITY_UNIT] @@ -164,7 +164,7 @@ class MeteoFranceRainSensor(MeteoFranceSensor): """Representation of a Meteo-France rain sensor.""" @property - def state(self): + def native_value(self): """Return the state.""" # search first cadran with rain next_rain = next( @@ -202,7 +202,7 @@ class MeteoFranceAlertSensor(MeteoFranceSensor): self._unique_id = self._name @property - def state(self): + def native_value(self): """Return the state.""" return get_warning_text_status_from_indice_color( self.coordinator.data.get_domain_max_color() diff --git a/homeassistant/components/meteoclimatic/sensor.py b/homeassistant/components/meteoclimatic/sensor.py index 101b889498d..b5a07ad06e6 100644 --- a/homeassistant/components/meteoclimatic/sensor.py +++ b/homeassistant/components/meteoclimatic/sensor.py @@ -51,7 +51,9 @@ class MeteoclimaticSensor(CoordinatorEntity, SensorEntity): f"{station.name} {SENSOR_TYPES[sensor_type][SENSOR_TYPE_NAME]}" ) self._attr_unique_id = f"{station.code}_{sensor_type}" - self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type].get(SENSOR_TYPE_UNIT) + self._attr_native_unit_of_measurement = SENSOR_TYPES[sensor_type].get( + SENSOR_TYPE_UNIT + ) @property def device_info(self): @@ -65,7 +67,7 @@ class MeteoclimaticSensor(CoordinatorEntity, SensorEntity): } @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return ( getattr(self.coordinator.data["weather"], self._type) diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index 749282b1a21..4919e36bd58 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -42,7 +42,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="name", name="Station Name", device_class=None, - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:label-outline", entity_registry_enabled_default=False, ), @@ -50,7 +50,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="weather", name="Weather", device_class=None, - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:weather-sunny", # but will adapt to current conditions entity_registry_enabled_default=True, ), @@ -58,7 +58,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="temperature", name="Temperature", device_class=DEVICE_CLASS_TEMPERATURE, - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, icon=None, entity_registry_enabled_default=True, ), @@ -66,7 +66,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="feels_like_temperature", name="Feels Like Temperature", device_class=DEVICE_CLASS_TEMPERATURE, - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, icon=None, entity_registry_enabled_default=False, ), @@ -74,7 +74,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="wind_speed", name="Wind Speed", device_class=None, - unit_of_measurement=SPEED_MILES_PER_HOUR, + native_unit_of_measurement=SPEED_MILES_PER_HOUR, icon="mdi:weather-windy", entity_registry_enabled_default=True, ), @@ -82,7 +82,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="wind_direction", name="Wind Direction", device_class=None, - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:compass-outline", entity_registry_enabled_default=False, ), @@ -90,7 +90,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="wind_gust", name="Wind Gust", device_class=None, - unit_of_measurement=SPEED_MILES_PER_HOUR, + native_unit_of_measurement=SPEED_MILES_PER_HOUR, icon="mdi:weather-windy", entity_registry_enabled_default=False, ), @@ -98,7 +98,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="visibility", name="Visibility", device_class=None, - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:eye", entity_registry_enabled_default=False, ), @@ -106,7 +106,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="visibility_distance", name="Visibility Distance", device_class=None, - unit_of_measurement=LENGTH_KILOMETERS, + native_unit_of_measurement=LENGTH_KILOMETERS, icon="mdi:eye", entity_registry_enabled_default=False, ), @@ -114,7 +114,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="uv", name="UV Index", device_class=None, - unit_of_measurement=UV_INDEX, + native_unit_of_measurement=UV_INDEX, icon="mdi:weather-sunny-alert", entity_registry_enabled_default=True, ), @@ -122,7 +122,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="precipitation", name="Probability of Precipitation", device_class=None, - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:weather-rainy", entity_registry_enabled_default=True, ), @@ -130,7 +130,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="humidity", name="Humidity", device_class=DEVICE_CLASS_HUMIDITY, - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon=None, entity_registry_enabled_default=False, ), @@ -189,7 +189,7 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): self.use_3hourly = use_3hourly @property - def state(self): + def native_value(self): """Return the state of the sensor.""" value = None diff --git a/homeassistant/components/mfi/sensor.py b/homeassistant/components/mfi/sensor.py index fafaf53ff99..b27f719d974 100644 --- a/homeassistant/components/mfi/sensor.py +++ b/homeassistant/components/mfi/sensor.py @@ -88,7 +88,7 @@ class MfiSensor(SensorEntity): return self._port.label @property - def state(self): + def native_value(self): """Return the state of the sensor.""" try: tag = self._port.tag @@ -115,7 +115,7 @@ class MfiSensor(SensorEntity): return None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" try: tag = self._port.tag diff --git a/homeassistant/components/mhz19/sensor.py b/homeassistant/components/mhz19/sensor.py index 63a1181f720..7d5d5eba183 100644 --- a/homeassistant/components/mhz19/sensor.py +++ b/homeassistant/components/mhz19/sensor.py @@ -90,12 +90,12 @@ class MHZ19Sensor(SensorEntity): return f"{self._name}: {SENSOR_TYPES[self._sensor_type][0]}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._ppm if self._sensor_type == SENSOR_CO2 else self._temperature @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index a7aab41bea9..f712ffe6fe5 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -180,7 +180,7 @@ class MiFloraSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -207,7 +207,7 @@ class MiFloraSensor(SensorEntity): return STATE_CLASS_MEASUREMENT @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the units of measurement.""" return self._unit diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index d103ff8eaa6..e4b4cdf9922 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -172,7 +172,7 @@ class MinMaxSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._unit_of_measurement_mismatch: return None @@ -181,7 +181,7 @@ class MinMaxSensor(SensorEntity): ) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" if self._unit_of_measurement_mismatch: return "ERR" diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py index 651c2762c55..9f1c89f09c6 100644 --- a/homeassistant/components/minecraft_server/sensor.py +++ b/homeassistant/components/minecraft_server/sensor.py @@ -70,12 +70,12 @@ class MinecraftServerSensorEntity(MinecraftServerEntity, SensorEntity): return self._server.online @property - def state(self) -> Any: + def native_value(self) -> Any: """Return sensor state.""" return self._state @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return sensor measurement unit.""" return self._unit diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py index 670a6daf3d3..732beb11b3a 100644 --- a/homeassistant/components/mitemp_bt/sensor.py +++ b/homeassistant/components/mitemp_bt/sensor.py @@ -127,12 +127,12 @@ class MiTempBtSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the units of measurement.""" return self._unit diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index 7e3c1c13148..f6652f7f889 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -74,11 +74,11 @@ class MobileAppSensor(MobileAppEntity, SensorEntity): """Representation of an mobile app sensor.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._config[ATTR_SENSOR_STATE] @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return self._config.get(ATTR_SENSOR_UOM) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index fee3f53667d..3165f416a6e 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -47,14 +47,14 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): ) -> None: """Initialize the modbus register sensor.""" super().__init__(hub, entry) - self._attr_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) + self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) async def async_added_to_hass(self): """Handle entity which will be added.""" await self.async_base_added_to_hass() state = await self.async_get_last_state() if state: - self._attr_state = state.state + self._attr_native_value = state.state async def async_update(self, now=None): """Update the state of the sensor.""" @@ -68,6 +68,6 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): self.async_write_ha_state() return - self._attr_state = self.unpack_structure_result(result.registers) + self._attr_native_value = self.unpack_structure_result(result.registers) self._attr_available = True self.async_write_ha_state() diff --git a/homeassistant/components/modem_callerid/sensor.py b/homeassistant/components/modem_callerid/sensor.py index 080e077a457..afbc09eb45c 100644 --- a/homeassistant/components/modem_callerid/sensor.py +++ b/homeassistant/components/modem_callerid/sensor.py @@ -75,7 +75,7 @@ class ModemCalleridSensor(SensorEntity): return ICON @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/modern_forms/sensor.py b/homeassistant/components/modern_forms/sensor.py index 01efe3f1d28..1e51ec9a1ae 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 state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the sensor.""" sleep_time: datetime = dt_util.utc_from_timestamp( self.coordinator.data.state.light_sleep_timer @@ -103,7 +103,7 @@ class ModernFormsFanTimerRemainingTimeSensor(ModernFormsSensor): self._attr_device_class = DEVICE_CLASS_TIMESTAMP @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the sensor.""" sleep_time: datetime = dt_util.utc_from_timestamp( self.coordinator.data.state.fan_sleep_timer diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index 7bfa161f9ec..c57903ce5b7 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -359,12 +359,12 @@ class MoldIndicator(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return PERCENTAGE @property - def state(self): + def native_value(self): """Return the state of the entity.""" return self._state diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index 6213e218d24..223ee831779 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -60,7 +60,7 @@ class MoonSensor(SensorEntity): return "moon__phase" @property - def state(self): + def native_value(self): """Return the state of the device.""" if self._state == 0: return STATE_NEW_MOON diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index be88a099f25..9c6db5d88ec 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -47,7 +47,7 @@ class MotionBatterySensor(CoordinatorEntity, SensorEntity): """ _attr_device_class = DEVICE_CLASS_BATTERY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, coordinator, blind): """Initialize the Motion Battery Sensor.""" @@ -70,7 +70,7 @@ class MotionBatterySensor(CoordinatorEntity, SensorEntity): return self.coordinator.data[self._blind.mac][ATTR_AVAILABLE] @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._blind.battery_level @@ -106,7 +106,7 @@ class MotionTDBUBatterySensor(MotionBatterySensor): self._attr_name = f"{blind.blind_type}-{motor}-battery-{blind.mac[12:]}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._blind.battery_level is None: return None @@ -128,7 +128,7 @@ class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity): _attr_device_class = DEVICE_CLASS_SIGNAL_STRENGTH _attr_entity_registry_enabled_default = False - _attr_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT + _attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT def __init__(self, coordinator, device, device_type): """Initialize the Motion Signal Strength Sensor.""" @@ -162,7 +162,7 @@ class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity): ) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.RSSI diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 239af7b450a..eac136d3f84 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -214,7 +214,7 @@ class MqttSensor(MqttEntity, SensorEntity): self.async_write_ha_state() @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return self._config.get(CONF_UNIT_OF_MEASUREMENT) @@ -224,7 +224,7 @@ class MqttSensor(MqttEntity, SensorEntity): return self._config[CONF_FORCE_UPDATE] @property - def state(self): + def native_value(self): """Return the state of the entity.""" return self._state diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index b40d550abf6..479b02ebcbd 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -139,7 +139,7 @@ class MQTTRoomSensor(SensorEntity): return {ATTR_DISTANCE: self._distance} @property - def state(self): + def native_value(self): """Return the current room of the entity.""" return self._state diff --git a/homeassistant/components/mvglive/sensor.py b/homeassistant/components/mvglive/sensor.py index 953fe4c69a8..416ce21cbaf 100644 --- a/homeassistant/components/mvglive/sensor.py +++ b/homeassistant/components/mvglive/sensor.py @@ -108,7 +108,7 @@ class MVGLiveSensor(SensorEntity): return self._station @property - def state(self): + def native_value(self): """Return the next departure time.""" return self._state @@ -128,7 +128,7 @@ class MVGLiveSensor(SensorEntity): return self._icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return TIME_MINUTES diff --git a/homeassistant/components/mychevy/sensor.py b/homeassistant/components/mychevy/sensor.py index 18b5e95d838..1a5613d8864 100644 --- a/homeassistant/components/mychevy/sensor.py +++ b/homeassistant/components/mychevy/sensor.py @@ -98,7 +98,7 @@ class MyChevyStatus(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state.""" return self._state @@ -166,7 +166,7 @@ class EVSensor(SensorEntity): self.async_write_ha_state() @property - def state(self): + def native_value(self): """Return the state.""" return self._state @@ -176,7 +176,7 @@ class EVSensor(SensorEntity): return self._state_attributes @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement the state is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index c7755b13512..bb56770fd0c 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -147,8 +147,8 @@ class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity): return True @property - def state(self) -> str | None: - """Return the state of this entity.""" + def native_value(self) -> str | None: + """Return the state of the device.""" return self._values.get(self.value_type) @property @@ -176,7 +176,7 @@ class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity): return self._get_sensor_type()[3] @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity.""" set_req = self.gateway.const.SetReq if ( diff --git a/tests/components/litterrobot/test_sensor.py b/tests/components/litterrobot/test_sensor.py index a5f5b955882..dbc8c39790c 100644 --- a/tests/components/litterrobot/test_sensor.py +++ b/tests/components/litterrobot/test_sensor.py @@ -26,6 +26,7 @@ async def test_sleep_time_sensor_with_none_state(hass): sensor = LitterRobotSleepTimeSensor( robot, "Sleep Mode Start Time", Mock(), "sleep_mode_start_time" ) + sensor.hass = hass assert sensor assert sensor.state is None diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index 1f5cc5fd04f..4032e29b743 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -121,7 +121,9 @@ def port_fixture(): @pytest.fixture(name="sensor") def sensor_fixture(hass, port): """Sensor fixture.""" - return mfi.MfiSensor(port, hass) + sensor = mfi.MfiSensor(port, hass) + sensor.hass = hass + return sensor async def test_name(port, sensor): diff --git a/tests/components/mhz19/test_sensor.py b/tests/components/mhz19/test_sensor.py index e827b5dfbd2..26e9441f9fc 100644 --- a/tests/components/mhz19/test_sensor.py +++ b/tests/components/mhz19/test_sensor.py @@ -83,10 +83,11 @@ async def aiohttp_client_update_good_read(mock_function): @patch("pmsensor.co2sensor.read_mh_z19_with_temperature", return_value=(1000, 24)) -async def test_co2_sensor(mock_function): +async def test_co2_sensor(mock_function, hass): """Test CO2 sensor.""" client = mhz19.MHZClient(co2sensor, "test.serial") sensor = mhz19.MHZ19Sensor(client, mhz19.SENSOR_CO2, None, "name") + sensor.hass = hass sensor.update() assert sensor.name == "name: CO2" @@ -97,10 +98,11 @@ async def test_co2_sensor(mock_function): @patch("pmsensor.co2sensor.read_mh_z19_with_temperature", return_value=(1000, 24)) -async def test_temperature_sensor(mock_function): +async def test_temperature_sensor(mock_function, hass): """Test temperature sensor.""" client = mhz19.MHZClient(co2sensor, "test.serial") sensor = mhz19.MHZ19Sensor(client, mhz19.SENSOR_TEMPERATURE, None, "name") + sensor.hass = hass sensor.update() assert sensor.name == "name: Temperature" @@ -111,12 +113,13 @@ async def test_temperature_sensor(mock_function): @patch("pmsensor.co2sensor.read_mh_z19_with_temperature", return_value=(1000, 24)) -async def test_temperature_sensor_f(mock_function): +async def test_temperature_sensor_f(mock_function, hass): """Test temperature sensor.""" client = mhz19.MHZClient(co2sensor, "test.serial") sensor = mhz19.MHZ19Sensor( client, mhz19.SENSOR_TEMPERATURE, TEMP_FAHRENHEIT, "name" ) + sensor.hass = hass sensor.update() assert sensor.state == 75.2 From 539ed56000d27e1e6fc2256000cc68457236677e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Wed, 11 Aug 2021 22:40:04 +0200 Subject: [PATCH 166/355] Refactor Fronius sensor device class and long term statistics (#54185) --- homeassistant/components/fronius/sensor.py | 101 +++++++-------------- 1 file changed, 35 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 6f949334d02..211fdaabafd 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -12,7 +12,6 @@ from homeassistant.components.sensor import ( PLATFORM_SCHEMA, STATE_CLASS_MEASUREMENT, SensorEntity, - SensorEntityDescription, ) from homeassistant.const import ( CONF_DEVICE, @@ -20,8 +19,14 @@ from homeassistant.const import ( CONF_RESOURCE, CONF_SCAN_INTERVAL, CONF_SENSOR_TYPE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_VOLTAGE, ) from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -48,6 +53,17 @@ DEFAULT_SCAN_INTERVAL = timedelta(seconds=60) SENSOR_TYPES = [TYPE_INVERTER, TYPE_STORAGE, TYPE_METER, TYPE_POWER_FLOW] SCOPE_TYPES = [SCOPE_DEVICE, SCOPE_SYSTEM] +PREFIX_DEVICE_CLASS_MAPPING = [ + ("state_of_charge", DEVICE_CLASS_BATTERY), + ("temperature", DEVICE_CLASS_TEMPERATURE), + ("power_factor", DEVICE_CLASS_POWER_FACTOR), + ("power", DEVICE_CLASS_POWER), + ("energy", DEVICE_CLASS_ENERGY), + ("current", DEVICE_CLASS_CURRENT), + ("timestamp", DEVICE_CLASS_TIMESTAMP), + ("voltage", DEVICE_CLASS_VOLTAGE), +] + def _device_id_validator(config): """Ensure that inverters have default id 1 and other devices 0.""" @@ -161,12 +177,6 @@ class FroniusAdapter: """Whether the fronius device is active.""" return self._available - def entity_description( # pylint: disable=no-self-use - self, key - ) -> SensorEntityDescription | None: - """Create entity description for a key.""" - return None - async def async_update(self): """Retrieve and update latest state.""" try: @@ -223,18 +233,6 @@ class FroniusAdapter: class FroniusInverterSystem(FroniusAdapter): """Adapter for the fronius inverter with system scope.""" - def entity_description(self, key): - """Return the entity descriptor.""" - if key != "energy_total": - return None - - return SensorEntityDescription( - key=key, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset=dt.utc_from_timestamp(0), - ) - async def _update(self): """Get the values for the current state.""" return await self.bridge.current_system_inverter_data() @@ -243,18 +241,6 @@ class FroniusInverterSystem(FroniusAdapter): class FroniusInverterDevice(FroniusAdapter): """Adapter for the fronius inverter with device scope.""" - def entity_description(self, key): - """Return the entity descriptor.""" - if key != "energy_total": - return None - - return SensorEntityDescription( - key=key, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset=dt.utc_from_timestamp(0), - ) - async def _update(self): """Get the values for the current state.""" return await self.bridge.current_inverter_data(self._device) @@ -271,18 +257,6 @@ class FroniusStorage(FroniusAdapter): class FroniusMeterSystem(FroniusAdapter): """Adapter for the fronius meter with system scope.""" - def entity_description(self, key): - """Return the entity descriptor.""" - if not key.startswith("energy_real_"): - return None - - return SensorEntityDescription( - key=key, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset=dt.utc_from_timestamp(0), - ) - async def _update(self): """Get the values for the current state.""" return await self.bridge.current_system_meter_data() @@ -291,18 +265,6 @@ class FroniusMeterSystem(FroniusAdapter): class FroniusMeterDevice(FroniusAdapter): """Adapter for the fronius meter with device scope.""" - def entity_description(self, key): - """Return the entity descriptor.""" - if not key.startswith("energy_real_"): - return None - - return SensorEntityDescription( - key=key, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset=dt.utc_from_timestamp(0), - ) - async def _update(self): """Get the values for the current state.""" return await self.bridge.current_meter_data(self._device) @@ -311,14 +273,6 @@ class FroniusMeterDevice(FroniusAdapter): class FroniusPowerFlow(FroniusAdapter): """Adapter for the fronius power flow.""" - def entity_description(self, key): - """Return the entity descriptor.""" - return SensorEntityDescription( - key=key, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, - ) - async def _update(self): """Get the values for the current state.""" return await self.bridge.current_power_flow() @@ -327,13 +281,17 @@ class FroniusPowerFlow(FroniusAdapter): class FroniusTemplateSensor(SensorEntity): """Sensor for the single values (e.g. pv power, ac power).""" - def __init__(self, parent: FroniusAdapter, key): + _attr_state_class = STATE_CLASS_MEASUREMENT + + def __init__(self, parent: FroniusAdapter, key: str) -> None: """Initialize a singular value sensor.""" self._key = key self._attr_name = f"{key.replace('_', ' ').capitalize()} {parent.name}" self._parent = parent - if entity_description := parent.entity_description(key): - self.entity_description = entity_description + for prefix, device_class in PREFIX_DEVICE_CLASS_MAPPING: + if self._key.startswith(prefix): + self._attr_device_class = device_class + break @property def should_poll(self): @@ -353,6 +311,17 @@ class FroniusTemplateSensor(SensorEntity): self._attr_state = round(self._attr_state, 2) self._attr_unit_of_measurement = state.get("unit") + @property + def last_reset(self) -> dt.dt.datetime | None: + """Return the time when the sensor was last reset, if it is a meter.""" + if self._key.endswith("day"): + return dt.start_of_local_day() + if self._key.endswith("year"): + return dt.start_of_local_day(dt.dt.date(dt.now().year, 1, 1)) + if self._key.endswith("total") or self._key.startswith("energy_real"): + return dt.utc_from_timestamp(0) + return None + async def async_added_to_hass(self): """Register at parent component for updates.""" self.async_on_remove(self._parent.register(self)) From b4113728723847174535d1acb24d4a14767c274e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 11 Aug 2021 22:41:51 +0200 Subject: [PATCH 167/355] Use EntityDescription - blink (#54360) --- .../components/blink/binary_sensor.py | 47 +++++++++++------- homeassistant/components/blink/sensor.py | 49 +++++++++++-------- 2 files changed, 59 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/blink/binary_sensor.py b/homeassistant/components/blink/binary_sensor.py index f9b8ec31605..6be284e2197 100644 --- a/homeassistant/components/blink/binary_sensor.py +++ b/homeassistant/components/blink/binary_sensor.py @@ -1,47 +1,60 @@ """Support for Blink system camera control.""" +from __future__ import annotations + from homeassistant.components.binary_sensor import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_MOTION, BinarySensorEntity, + BinarySensorEntityDescription, ) from .const import DOMAIN, TYPE_BATTERY, TYPE_CAMERA_ARMED, TYPE_MOTION_DETECTED -BINARY_SENSORS = { - TYPE_BATTERY: ["Battery", DEVICE_CLASS_BATTERY], - TYPE_CAMERA_ARMED: ["Camera Armed", None], - TYPE_MOTION_DETECTED: ["Motion Detected", DEVICE_CLASS_MOTION], -} +BINARY_SENSORS_TYPES: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription( + key=TYPE_BATTERY, + name="Battery", + device_class=DEVICE_CLASS_BATTERY, + ), + BinarySensorEntityDescription( + key=TYPE_CAMERA_ARMED, + name="Camera Armed", + ), + BinarySensorEntityDescription( + key=TYPE_MOTION_DETECTED, + name="Motion Detected", + device_class=DEVICE_CLASS_MOTION, + ), +) async def async_setup_entry(hass, config, async_add_entities): """Set up the blink binary sensors.""" data = hass.data[DOMAIN][config.entry_id] - entities = [] - for camera in data.cameras: - for sensor_type in BINARY_SENSORS: - entities.append(BlinkBinarySensor(data, camera, sensor_type)) + entities = [ + BlinkBinarySensor(data, camera, description) + for camera in data.cameras + for description in BINARY_SENSORS_TYPES + ] async_add_entities(entities) class BlinkBinarySensor(BinarySensorEntity): """Representation of a Blink binary sensor.""" - def __init__(self, data, camera, sensor_type): + def __init__(self, data, camera, description: BinarySensorEntityDescription): """Initialize the sensor.""" self.data = data - self._type = sensor_type - name, device_class = BINARY_SENSORS[sensor_type] - self._attr_name = f"{DOMAIN} {camera} {name}" - self._attr_device_class = device_class + self.entity_description = description + self._attr_name = f"{DOMAIN} {camera} {description.name}" self._camera = data.cameras[camera] - self._attr_unique_id = f"{self._camera.serial}-{sensor_type}" + self._attr_unique_id = f"{self._camera.serial}-{description.key}" def update(self): """Update sensor state.""" self.data.refresh() - state = self._camera.attributes[self._type] - if self._type == TYPE_BATTERY: + state = self._camera.attributes[self.entity_description.key] + if self.entity_description.key == TYPE_BATTERY: state = state != "ok" self._attr_is_on = state diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py index 88f10183b32..d2122b59cd8 100644 --- a/homeassistant/components/blink/sensor.py +++ b/homeassistant/components/blink/sensor.py @@ -1,7 +1,9 @@ """Support for Blink system camera sensors.""" +from __future__ import annotations + import logging -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import ( DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, @@ -13,23 +15,30 @@ from .const import DOMAIN, TYPE_TEMPERATURE, TYPE_WIFI_STRENGTH _LOGGER = logging.getLogger(__name__) -SENSORS = { - TYPE_TEMPERATURE: ["Temperature", TEMP_FAHRENHEIT, DEVICE_CLASS_TEMPERATURE], - TYPE_WIFI_STRENGTH: [ - "Wifi Signal", - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - DEVICE_CLASS_SIGNAL_STRENGTH, - ], -} +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key=TYPE_TEMPERATURE, + name="Temperature", + native_unit_of_measurement=TEMP_FAHRENHEIT, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + SensorEntityDescription( + key=TYPE_WIFI_STRENGTH, + name="Wifi Signal", + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + ), +) async def async_setup_entry(hass, config, async_add_entities): """Initialize a Blink sensor.""" data = hass.data[DOMAIN][config.entry_id] - entities = [] - for camera in data.cameras: - for sensor_type in SENSORS: - entities.append(BlinkSensor(data, camera, sensor_type)) + entities = [ + BlinkSensor(data, camera, description) + for camera in data.cameras + for description in SENSOR_TYPES + ] async_add_entities(entities) @@ -37,17 +46,17 @@ async def async_setup_entry(hass, config, async_add_entities): class BlinkSensor(SensorEntity): """A Blink camera sensor.""" - def __init__(self, data, camera, sensor_type): + def __init__(self, data, camera, description: SensorEntityDescription): """Initialize sensors from Blink camera.""" - name, units, device_class = SENSORS[sensor_type] - self._attr_name = f"{DOMAIN} {camera} {name}" - self._attr_device_class = device_class + self.entity_description = description + self._attr_name = f"{DOMAIN} {camera} {description.name}" self.data = data self._camera = data.cameras[camera] - self._attr_native_unit_of_measurement = units - self._attr_unique_id = f"{self._camera.serial}-{sensor_type}" + self._attr_unique_id = f"{self._camera.serial}-{description.key}" self._sensor_key = ( - "temperature_calibrated" if sensor_type == "temperature" else sensor_type + "temperature_calibrated" + if description.key == "temperature" + else description.key ) def update(self): From f77187d28ab50448b8e8e0bce181fa9f86fb0b80 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 12 Aug 2021 00:16:28 +0200 Subject: [PATCH 168/355] Deprecate Wink integration (#54496) --- homeassistant/components/wink/__init__.py | 45 +++++++++++++---------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index f11e15670e9..f346d9145f8 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -111,25 +111,28 @@ CHIME_TONES = TONES + ["inactive"] AUTO_SHUTOFF_TIMES = [None, -1, 30, 60, 120] CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Inclusive( - CONF_EMAIL, CONF_OAUTH, msg=CONF_MISSING_OAUTH_MSG - ): cv.string, - vol.Inclusive( - CONF_PASSWORD, CONF_OAUTH, msg=CONF_MISSING_OAUTH_MSG - ): cv.string, - vol.Inclusive( - CONF_CLIENT_ID, CONF_OAUTH, msg=CONF_MISSING_OAUTH_MSG - ): cv.string, - vol.Inclusive( - CONF_CLIENT_SECRET, CONF_OAUTH, msg=CONF_MISSING_OAUTH_MSG - ): cv.string, - vol.Optional(CONF_LOCAL_CONTROL, default=False): cv.boolean, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Inclusive( + CONF_EMAIL, CONF_OAUTH, msg=CONF_MISSING_OAUTH_MSG + ): cv.string, + vol.Inclusive( + CONF_PASSWORD, CONF_OAUTH, msg=CONF_MISSING_OAUTH_MSG + ): cv.string, + vol.Inclusive( + CONF_CLIENT_ID, CONF_OAUTH, msg=CONF_MISSING_OAUTH_MSG + ): cv.string, + vol.Inclusive( + CONF_CLIENT_SECRET, CONF_OAUTH, msg=CONF_MISSING_OAUTH_MSG + ): cv.string, + vol.Optional(CONF_LOCAL_CONTROL, default=False): cv.boolean, + } + ), + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -282,6 +285,10 @@ def _request_oauth_completion(hass, config): def setup(hass, config): # noqa: C901 """Set up the Wink component.""" + _LOGGER.warning( + "The Wink integration has been deprecated and is pending removal in " + "Home Assistant Core 2021.11" + ) if hass.data.get(DOMAIN) is None: hass.data[DOMAIN] = { From 0626542a143931de9435b95a8b5dfa9884da0cc5 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 12 Aug 2021 00:14:10 +0000 Subject: [PATCH 169/355] [ci skip] Translation update --- .../components/adax/translations/zh-Hans.json | 14 +++++++ .../august/translations/zh-Hans.json | 16 +++++++ .../azure_devops/translations/zh-Hans.json | 26 +++++++++++- .../bosch_shc/translations/zh-Hans.json | 14 +++++++ .../braviatv/translations/zh-Hans.json | 14 ++++++- .../cloudflare/translations/zh-Hans.json | 5 +++ .../elgato/translations/zh-Hans.json | 15 +++++++ .../energy/translations/zh-Hans.json | 3 ++ .../enphase_envoy/translations/zh-Hans.json | 11 +++++ .../epson/translations/zh-Hans.json | 16 +++++++ .../components/flipr/translations/es.json | 8 +++- .../flipr/translations/zh-Hans.json | 11 +++++ .../flume/translations/zh-Hans.json | 6 +++ .../forked_daapd/translations/zh-Hans.json | 25 +++++++++++ .../fritz/translations/zh-Hans.json | 27 ++++++++++++ .../growatt_server/translations/zh-Hans.json | 11 +++++ .../components/hive/translations/zh-Hans.json | 19 +++++++++ .../honeywell/translations/zh-Hans.json | 11 +++++ .../ifttt/translations/zh-Hans.json | 4 ++ .../kmtronic/translations/zh-Hans.json | 11 +++++ .../translations/zh-Hans.json | 11 +++++ .../kraken/translations/es-419.json | 12 ++++++ .../components/kraken/translations/es.json | 10 +++++ .../components/litejet/translations/es.json | 10 +++++ .../litejet/translations/zh-Hans.json | 9 ++++ .../litterrobot/translations/zh-Hans.json | 11 +++++ .../components/myq/translations/zh-Hans.json | 6 +++ .../components/nut/translations/zh-Hans.json | 32 +++++++++++++- .../onvif/translations/zh-Hans.json | 2 +- .../ovo_energy/translations/zh-Hans.json | 7 ++++ .../prosegur/translations/zh-Hans.json | 29 +++++++++++++ .../renault/translations/es-419.json | 9 ++++ .../components/renault/translations/es.json | 12 +++++- .../renault/translations/zh-Hans.json | 1 + .../translations/zh-Hans.json | 11 +++++ .../roomba/translations/zh-Hans.json | 12 ++++++ .../components/sensor/translations/en.json | 2 + .../components/sensor/translations/et.json | 2 + .../sensor/translations/zh-Hant.json | 2 + .../shopping_list/translations/zh-Hans.json | 14 +++++++ .../smartthings/translations/zh-Hans.json | 5 +++ .../smarttub/translations/zh-Hans.json | 11 +++++ .../components/soma/translations/zh-Hans.json | 7 ++++ .../sonarr/translations/zh-Hans.json | 38 +++++++++++++++++ .../srp_energy/translations/zh-Hans.json | 13 ++++++ .../subaru/translations/zh-Hans.json | 11 +++++ .../components/tesla/translations/es.json | 1 + .../tesla/translations/zh-Hans.json | 11 +++++ .../uptimerobot/translations/es-419.json | 7 ++++ .../uptimerobot/translations/es.json | 13 +++++- .../uptimerobot/translations/zh-Hans.json | 12 +++++- .../verisure/translations/zh-Hans.json | 16 +++++++ .../wallbox/translations/zh-Hans.json | 11 +++++ .../xiaomi_miio/translations/zh-Hans.json | 3 +- .../yale_smart_alarm/translations/es-419.json | 11 +++++ .../translations/zh-Hans.json | 28 +++++++++++++ .../translations/zh-Hans.json | 9 ++++ .../yeelight/translations/zh-Hans.json | 42 +++++++++++++++++++ .../youless/translations/zh-Hans.json | 15 +++++++ .../zoneminder/translations/zh-Hans.json | 6 +++ 60 files changed, 729 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/adax/translations/zh-Hans.json create mode 100644 homeassistant/components/august/translations/zh-Hans.json create mode 100644 homeassistant/components/bosch_shc/translations/zh-Hans.json create mode 100644 homeassistant/components/energy/translations/zh-Hans.json create mode 100644 homeassistant/components/enphase_envoy/translations/zh-Hans.json create mode 100644 homeassistant/components/epson/translations/zh-Hans.json create mode 100644 homeassistant/components/flipr/translations/zh-Hans.json create mode 100644 homeassistant/components/forked_daapd/translations/zh-Hans.json create mode 100644 homeassistant/components/fritz/translations/zh-Hans.json create mode 100644 homeassistant/components/growatt_server/translations/zh-Hans.json create mode 100644 homeassistant/components/hive/translations/zh-Hans.json create mode 100644 homeassistant/components/honeywell/translations/zh-Hans.json create mode 100644 homeassistant/components/kmtronic/translations/zh-Hans.json create mode 100644 homeassistant/components/kostal_plenticore/translations/zh-Hans.json create mode 100644 homeassistant/components/kraken/translations/es-419.json create mode 100644 homeassistant/components/litejet/translations/zh-Hans.json create mode 100644 homeassistant/components/litterrobot/translations/zh-Hans.json create mode 100644 homeassistant/components/prosegur/translations/zh-Hans.json create mode 100644 homeassistant/components/renault/translations/es-419.json create mode 100644 homeassistant/components/rituals_perfume_genie/translations/zh-Hans.json create mode 100644 homeassistant/components/roomba/translations/zh-Hans.json create mode 100644 homeassistant/components/shopping_list/translations/zh-Hans.json create mode 100644 homeassistant/components/smarttub/translations/zh-Hans.json create mode 100644 homeassistant/components/soma/translations/zh-Hans.json create mode 100644 homeassistant/components/sonarr/translations/zh-Hans.json create mode 100644 homeassistant/components/srp_energy/translations/zh-Hans.json create mode 100644 homeassistant/components/subaru/translations/zh-Hans.json create mode 100644 homeassistant/components/tesla/translations/zh-Hans.json create mode 100644 homeassistant/components/uptimerobot/translations/es-419.json create mode 100644 homeassistant/components/verisure/translations/zh-Hans.json create mode 100644 homeassistant/components/wallbox/translations/zh-Hans.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/es-419.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/zh-Hans.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/zh-Hans.json create mode 100644 homeassistant/components/yeelight/translations/zh-Hans.json create mode 100644 homeassistant/components/youless/translations/zh-Hans.json diff --git a/homeassistant/components/adax/translations/zh-Hans.json b/homeassistant/components/adax/translations/zh-Hans.json new file mode 100644 index 00000000000..7356ec08b15 --- /dev/null +++ b/homeassistant/components/adax/translations/zh-Hans.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/zh-Hans.json b/homeassistant/components/august/translations/zh-Hans.json new file mode 100644 index 00000000000..b932dae2511 --- /dev/null +++ b/homeassistant/components/august/translations/zh-Hans.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_validate": { + "data": { + "password": "\u5bc6\u7801" + } + }, + "user_validate": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_devops/translations/zh-Hans.json b/homeassistant/components/azure_devops/translations/zh-Hans.json index b0c629646e2..d6a6e62e27c 100644 --- a/homeassistant/components/azure_devops/translations/zh-Hans.json +++ b/homeassistant/components/azure_devops/translations/zh-Hans.json @@ -1,8 +1,32 @@ { "config": { + "abort": { + "already_configured": "\u8d26\u6237\u5df2\u88ab\u914d\u7f6e", + "reauth_successful": "\u91cd\u9a8c\u8bc1\u6210\u529f" + }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", - "invalid_auth": "\u9a8c\u8bc1\u7801\u65e0\u6548" + "invalid_auth": "\u9a8c\u8bc1\u7801\u65e0\u6548", + "project_error": "\u65e0\u6cd5\u83b7\u53d6\u9879\u76ee\u4fe1\u606f\u3002" + }, + "flow_title": "{project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "\u4e2a\u4eba\u8bbf\u95ee\u4ee4\u724c (PAT)" + }, + "description": "{project_url} \u8eab\u4efd\u9a8c\u8bc1\u5931\u8d25\u3002\u8bf7\u8f93\u5165\u60a8\u5f53\u524d\u7684\u51ed\u636e\u3002", + "title": "\u91cd\u9a8c\u8bc1" + }, + "user": { + "data": { + "organization": "\u7ec4\u7ec7", + "personal_access_token": "\u4e2a\u4eba\u8bbf\u95ee\u4ee4\u724c (PAT)", + "project": "\u9879\u76ee" + }, + "description": "\u8bbe\u7f6e Azure DevOps \u5b9e\u4f8b\u4ee5\u8bbf\u95ee\u60a8\u7684\u9879\u76ee\u3002\u79c1\u4eba\u9879\u76ee\u624d\u9700\u8981\u63d0\u4f9b\u4e2a\u4eba\u8bbf\u95ee\u4ee4\u724c\u3002", + "title": "\u6dfb\u52a0 Azure DevOps \u9879\u76ee" + } } } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/zh-Hans.json b/homeassistant/components/bosch_shc/translations/zh-Hans.json new file mode 100644 index 00000000000..46682f56114 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/zh-Hans.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "pairing_failed": "\u914d\u5bf9\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u535a\u4e16 Smart Home Controller \u662f\u5426\u6b63\u5728\u5904\u4e8e\u914d\u5bf9\u6a21\u5f0f(LED \u706f\u95ea\u70c1)\uff0c\u4ee5\u53ca\u952e\u5165\u7684\u5bc6\u7801\u662f\u5426\u6b63\u786e" + }, + "step": { + "credentials": { + "data": { + "password": "Smart Home Controller \u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/zh-Hans.json b/homeassistant/components/braviatv/translations/zh-Hans.json index c839a271614..d02d562d55d 100644 --- a/homeassistant/components/braviatv/translations/zh-Hans.json +++ b/homeassistant/components/braviatv/translations/zh-Hans.json @@ -4,10 +4,20 @@ "authorize": { "data": { "pin": "PIN \u7801" - } + }, + "description": "\u8f93\u5165\u5728 Sony Bravia \u7535\u89c6\u4e0a\u663e\u793a\u7684 PIN \u7801\u3002 \n\n\u5982\u679c\u672a\u663e\u793a PIN \u7801\uff0c\u60a8\u9700\u8981\u5728\u7535\u89c6\u4e0a\u53d6\u6d88\u6ce8\u518c Home Assistant\uff0c\u8bf7\u8f6c\u5230\uff1a\u8bbe\u7f6e - >\u7f51\u7edc - >\u8fdc\u7a0b\u8bbe\u5907\u8bbe\u7f6e - >\u53d6\u6d88\u6ce8\u518c\u8fdc\u7a0b\u8bbe\u5907\u3002", + "title": "\u6388\u6743 Sony Bravia \u7535\u89c6" }, "user": { - "description": "\u8bbe\u7f6eSony Bravia\u7535\u89c6\u96c6\u6210\u3002\u5982\u679c\u60a8\u5728\u914d\u7f6e\u65b9\u9762\u9047\u5230\u95ee\u9898\uff0c\u8bf7\u8bbf\u95ee\uff1ahttps://www.home-assistant.io/integrations/braviatv\n\u786e\u4fdd\u7535\u89c6\u5df2\u6253\u5f00\u3002" + "description": "\u8bbe\u7f6e Sony Bravia \u7535\u89c6\u96c6\u6210\u3002\u5982\u679c\u60a8\u5728\u914d\u7f6e\u65b9\u9762\u9047\u5230\u95ee\u9898\uff0c\u8bf7\u8bbf\u95ee\uff1ahttps://www.home-assistant.io/integrations/braviatv\n\u786e\u4fdd\u7535\u89c6\u5df2\u6253\u5f00\u3002", + "title": "Sony Bravia TV" + } + } + }, + "options": { + "step": { + "user": { + "title": "Sony Bravia \u7535\u89c6\u9009\u9879" } } } diff --git a/homeassistant/components/cloudflare/translations/zh-Hans.json b/homeassistant/components/cloudflare/translations/zh-Hans.json index 4b0a696e5fc..78429184bad 100644 --- a/homeassistant/components/cloudflare/translations/zh-Hans.json +++ b/homeassistant/components/cloudflare/translations/zh-Hans.json @@ -5,6 +5,11 @@ "invalid_auth": "\u9a8c\u8bc1\u7801\u65e0\u6548" }, "step": { + "reauth_confirm": { + "data": { + "description": "\u4f7f\u7528\u60a8\u7684 Cloudflare \u5e10\u6237\u91cd\u65b0\u8fdb\u884c\u8eab\u4efd\u9a8c\u8bc1\u3002" + } + }, "user": { "data": { "api_token": "API \u5bc6\u7801" diff --git a/homeassistant/components/elgato/translations/zh-Hans.json b/homeassistant/components/elgato/translations/zh-Hans.json index 254f6df9327..94813c444eb 100644 --- a/homeassistant/components/elgato/translations/zh-Hans.json +++ b/homeassistant/components/elgato/translations/zh-Hans.json @@ -1,10 +1,25 @@ { "config": { "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", "cannot_connect": "\u8fde\u63a5\u5931\u8d25" }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + }, + "flow_title": "{serial_number}", + "step": { + "user": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "port": "\u7aef\u53e3" + }, + "description": "\u8bbe\u7f6e\u60a8\u7684 Elgato Light \u4ee5\u4e0e Home Assistant \u96c6\u6210\u3002" + }, + "zeroconf_confirm": { + "description": "\u60a8\u60f3\u5c06\u5e8f\u5217\u53f7\u4e3a `{serial_number}` \u7684 Elgato Light \u6dfb\u52a0\u5230 Home Assistant \u5417\uff1f", + "title": "\u53d1\u73b0 Elgato Light \u88c5\u7f6e" + } } } } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/zh-Hans.json b/homeassistant/components/energy/translations/zh-Hans.json new file mode 100644 index 00000000000..bae50fae66e --- /dev/null +++ b/homeassistant/components/energy/translations/zh-Hans.json @@ -0,0 +1,3 @@ +{ + "title": "\u80fd\u6e90" +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/zh-Hans.json b/homeassistant/components/enphase_envoy/translations/zh-Hans.json new file mode 100644 index 00000000000..d217ccdc842 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/epson/translations/zh-Hans.json b/homeassistant/components/epson/translations/zh-Hans.json new file mode 100644 index 00000000000..3cb7f97ceb9 --- /dev/null +++ b/homeassistant/components/epson/translations/zh-Hans.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "powered_off": "\u6295\u5f71\u4eea\u662f\u5426\u5df2\u7ecf\u6253\u5f00\uff1f\u60a8\u9700\u8981\u6253\u5f00\u6295\u5f71\u4eea\u4ee5\u8fdb\u884c\u521d\u59cb\u914d\u7f6e\u3002" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "name": "\u540d\u79f0" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/es.json b/homeassistant/components/flipr/translations/es.json index 478510ba5f1..56898d19a42 100644 --- a/homeassistant/components/flipr/translations/es.json +++ b/homeassistant/components/flipr/translations/es.json @@ -4,11 +4,17 @@ "unknown": "Error desconocido" }, "step": { + "flipr_id": { + "description": "Elija su ID de Flipr en la lista", + "title": "Elige tu Flipr" + }, "user": { "data": { "email": "Correo-e", "password": "Clave" - } + }, + "description": "Con\u00e9ctese usando su cuenta Flipr.", + "title": "Conectarse a Flipr" } } } diff --git a/homeassistant/components/flipr/translations/zh-Hans.json b/homeassistant/components/flipr/translations/zh-Hans.json new file mode 100644 index 00000000000..d217ccdc842 --- /dev/null +++ b/homeassistant/components/flipr/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/zh-Hans.json b/homeassistant/components/flume/translations/zh-Hans.json index a5f4ff11f09..db06c3cf23a 100644 --- a/homeassistant/components/flume/translations/zh-Hans.json +++ b/homeassistant/components/flume/translations/zh-Hans.json @@ -1,6 +1,12 @@ { "config": { "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u7801" + }, + "description": "{username} \u7684\u5bc6\u7801\u5df2\u5931\u6548\u3002" + }, "user": { "data": { "username": "\u7528\u6237\u540d" diff --git a/homeassistant/components/forked_daapd/translations/zh-Hans.json b/homeassistant/components/forked_daapd/translations/zh-Hans.json new file mode 100644 index 00000000000..9b2bd981397 --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/zh-Hans.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "not_forked_daapd": "\u6b64\u8bbe\u5907\u4e0d\u662f\u4e00\u4e2a forked-daapd \u670d\u52a1\u5668\u3002" + }, + "error": { + "forbidden": "\u65e0\u6cd5\u8fde\u63a5\u3002\u8bf7\u68c0\u67e5\u60a8\u7684 forked-daapd \u7f51\u7edc\u6743\u9650\u3002", + "websocket_not_enabled": "\u672a\u542f\u7528 forked-daapd \u670d\u52a1\u5668\u7684 Websocket \u529f\u80fd\u3002", + "wrong_server_type": "forked-daapd \u96c6\u6210\u9700\u8981 forked-daapd \u670d\u52a1\u5668\u7248\u672c\u53f7\u81f3\u5c11\u5927\u4e8e\u6216\u7b49\u4e8e 27.0 \u3002" + }, + "step": { + "user": { + "title": "\u8bbe\u7f6e forked-daapd \u8bbe\u5907" + } + } + }, + "options": { + "step": { + "init": { + "description": "\u4e3a forked-daapd \u96c6\u6210\u8bbe\u7f6e\u5404\u79cd\u9009\u9879\u3002", + "title": "\u914d\u7f6e forked-daapd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/zh-Hans.json b/homeassistant/components/fritz/translations/zh-Hans.json new file mode 100644 index 00000000000..91d68989675 --- /dev/null +++ b/homeassistant/components/fritz/translations/zh-Hans.json @@ -0,0 +1,27 @@ +{ + "config": { + "step": { + "confirm": { + "data": { + "password": "\u5bc6\u7801" + } + }, + "reauth_confirm": { + "data": { + "password": "\u5bc6\u7801" + } + }, + "start_config": { + "data": { + "password": "\u5bc6\u7801" + } + }, + "user": { + "data": { + "password": "\u5bc6\u7801" + }, + "description": "\u914d\u7f6e FRITZ!Box Tool \u4ee5\u63a7\u5236\u60a8\u7684 FRITZ!Box\u3002\n\u6700\u4f4e\u4fe1\u606f\u63d0\u4f9b\u8981\u6c42\uff1a\u7528\u6237\u540d\u3001\u5bc6\u7801\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/zh-Hans.json b/homeassistant/components/growatt_server/translations/zh-Hans.json new file mode 100644 index 00000000000..d217ccdc842 --- /dev/null +++ b/homeassistant/components/growatt_server/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/zh-Hans.json b/homeassistant/components/hive/translations/zh-Hans.json new file mode 100644 index 00000000000..780a47cb958 --- /dev/null +++ b/homeassistant/components/hive/translations/zh-Hans.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "invalid_password": "\u65e0\u6cd5\u767b\u5f55 Hive\uff0c\u5bc6\u7801\u9519\u8bef\uff0c\u8bf7\u91cd\u8bd5\u3002" + }, + "step": { + "reauth": { + "data": { + "password": "\u5bc6\u7801" + } + }, + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/zh-Hans.json b/homeassistant/components/honeywell/translations/zh-Hans.json new file mode 100644 index 00000000000..d217ccdc842 --- /dev/null +++ b/homeassistant/components/honeywell/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/translations/zh-Hans.json b/homeassistant/components/ifttt/translations/zh-Hans.json index c9e8bfd6044..78cbc37a7d9 100644 --- a/homeassistant/components/ifttt/translations/zh-Hans.json +++ b/homeassistant/components/ifttt/translations/zh-Hans.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "\u5b9e\u4f8b\u5df2\u914d\u7f6e\uff0c\u4e14\u53ea\u80fd\u5b58\u5728\u5355\u4e2a\u914d\u7f6e\u3002", + "webhook_not_internet_accessible": "Home Assistant \u9700\u8981\u7f51\u7edc\u8fde\u63a5\u4ee5\u83b7\u53d6\u76f8\u5173\u63a8\u9001\u4fe1\u606f\u3002" + }, "create_entry": { "default": "\u8981\u5411 Home Assistant \u53d1\u9001\u4e8b\u4ef6\uff0c\u60a8\u9700\u8981\u4f7f\u7528 [IFTTT Webhook applet]({applet_url}) \u4e2d\u7684 \"Make a web request\" \u52a8\u4f5c\u3002\n\n\u586b\u5199\u4ee5\u4e0b\u4fe1\u606f\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u6709\u5173\u5982\u4f55\u914d\u7f6e\u81ea\u52a8\u5316\u4ee5\u5904\u7406\u4f20\u5165\u7684\u6570\u636e\uff0c\u8bf7\u53c2\u9605[\u6587\u6863]({docs_url})\u3002" }, diff --git a/homeassistant/components/kmtronic/translations/zh-Hans.json b/homeassistant/components/kmtronic/translations/zh-Hans.json new file mode 100644 index 00000000000..d217ccdc842 --- /dev/null +++ b/homeassistant/components/kmtronic/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/zh-Hans.json b/homeassistant/components/kostal_plenticore/translations/zh-Hans.json new file mode 100644 index 00000000000..d217ccdc842 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/es-419.json b/homeassistant/components/kraken/translations/es-419.json new file mode 100644 index 00000000000..106ff98de0d --- /dev/null +++ b/homeassistant/components/kraken/translations/es-419.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "one": "", + "other": "Otros" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/es.json b/homeassistant/components/kraken/translations/es.json index afcf3f92d45..1befa14a52b 100644 --- a/homeassistant/components/kraken/translations/es.json +++ b/homeassistant/components/kraken/translations/es.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "user": { + "data": { + "one": "", + "other": "Otros" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/litejet/translations/es.json b/homeassistant/components/litejet/translations/es.json index 32d39e995e1..41875da9e69 100644 --- a/homeassistant/components/litejet/translations/es.json +++ b/homeassistant/components/litejet/translations/es.json @@ -15,5 +15,15 @@ "title": "Conectarse a LiteJet" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "Transici\u00f3n predeterminada (segundos)" + }, + "title": "Configurar LiteJet" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/zh-Hans.json b/homeassistant/components/litejet/translations/zh-Hans.json new file mode 100644 index 00000000000..133385be2d3 --- /dev/null +++ b/homeassistant/components/litejet/translations/zh-Hans.json @@ -0,0 +1,9 @@ +{ + "options": { + "step": { + "init": { + "title": "\u914d\u7f6e LiteJet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/zh-Hans.json b/homeassistant/components/litterrobot/translations/zh-Hans.json new file mode 100644 index 00000000000..d217ccdc842 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/translations/zh-Hans.json b/homeassistant/components/myq/translations/zh-Hans.json index a5f4ff11f09..db06c3cf23a 100644 --- a/homeassistant/components/myq/translations/zh-Hans.json +++ b/homeassistant/components/myq/translations/zh-Hans.json @@ -1,6 +1,12 @@ { "config": { "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u7801" + }, + "description": "{username} \u7684\u5bc6\u7801\u5df2\u5931\u6548\u3002" + }, "user": { "data": { "username": "\u7528\u6237\u540d" diff --git a/homeassistant/components/nut/translations/zh-Hans.json b/homeassistant/components/nut/translations/zh-Hans.json index 91522c7f609..4afd1ff0031 100644 --- a/homeassistant/components/nut/translations/zh-Hans.json +++ b/homeassistant/components/nut/translations/zh-Hans.json @@ -1,15 +1,34 @@ { "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, "step": { "resources": { "data": { "resources": "\u8d44\u6e90" - } + }, + "title": "\u9009\u62e9\u8981\u76d1\u89c6\u7684\u8d44\u6e90" + }, + "ups": { + "data": { + "alias": "\u522b\u540d", + "resources": "\u8d44\u6e90" + }, + "title": "\u9009\u62e9\u8981\u76d1\u63a7\u7684 UPS" }, "user": { "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "password": "\u5bc6\u7801", + "port": "\u7aef\u53e3", "username": "\u7528\u6237\u540d" - } + }, + "title": "\u8fde\u63a5\u5230 NUT \u670d\u52a1\u5668" } } }, @@ -17,6 +36,15 @@ "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", "unknown": "\u4e0d\u5728\u9884\u671f\u5185\u7684\u9519\u8bef" + }, + "step": { + "init": { + "data": { + "resources": "\u8d44\u6e90", + "scan_interval": "\u626b\u63cf\u95f4\u9694\uff08\u79d2\uff09" + }, + "description": "\u9009\u62e9\u8981\u76d1\u89c6\u7684\u8d44\u6e90" + } } } } \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/zh-Hans.json b/homeassistant/components/onvif/translations/zh-Hans.json index 13dd993228e..8ebde5a1bda 100644 --- a/homeassistant/components/onvif/translations/zh-Hans.json +++ b/homeassistant/components/onvif/translations/zh-Hans.json @@ -62,7 +62,7 @@ "onvif_devices": { "data": { "extra_arguments": "\u9644\u52a0 FFmpeg \u53c2\u6570", - "rtsp_transport": "RTSP \u4f20\u8f93" + "rtsp_transport": "RTSP \u4f20\u8f93\u901a\u8baf\u534f\u8bae" }, "title": "ONVIF \u8bbe\u5907\u9009\u9879" } diff --git a/homeassistant/components/ovo_energy/translations/zh-Hans.json b/homeassistant/components/ovo_energy/translations/zh-Hans.json index a7477e8c370..cf7a799531d 100644 --- a/homeassistant/components/ovo_energy/translations/zh-Hans.json +++ b/homeassistant/components/ovo_energy/translations/zh-Hans.json @@ -4,9 +4,16 @@ "cannot_connect": "\u8fde\u63a5\u5931\u8d25", "invalid_auth": "\u9a8c\u8bc1\u7801\u9519\u8bef" }, + "flow_title": "{username}", "step": { + "reauth": { + "data": { + "password": "\u5bc6\u7801" + } + }, "user": { "data": { + "password": "\u5bc6\u7801", "username": "\u7528\u6237\u540d" } } diff --git a/homeassistant/components/prosegur/translations/zh-Hans.json b/homeassistant/components/prosegur/translations/zh-Hans.json new file mode 100644 index 00000000000..426e1f2919b --- /dev/null +++ b/homeassistant/components/prosegur/translations/zh-Hans.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", + "reauth_successful": "\u91cd\u9a8c\u8bc1\u6210\u529f" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u6210\u529f", + "invalid_auth": "\u9a8c\u8bc1\u65e0\u6548", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "\u4f7f\u7528 Prosegur \u5e10\u6237\u91cd\u65b0\u8fdb\u884c\u8eab\u4efd\u9a8c\u8bc1\u3002", + "password": "\u5bc6\u7801", + "username": "\u7528\u6237\u540d" + } + }, + "user": { + "data": { + "country": "\u56fd\u5bb6/\u5730\u533a", + "password": "\u5bc6\u7801", + "username": "\u7528\u6237\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/es-419.json b/homeassistant/components/renault/translations/es-419.json new file mode 100644 index 00000000000..6c895416ef8 --- /dev/null +++ b/homeassistant/components/renault/translations/es-419.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Establecer las credenciales de Renault" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/es.json b/homeassistant/components/renault/translations/es.json index 894226d361e..0eabcacccd3 100644 --- a/homeassistant/components/renault/translations/es.json +++ b/homeassistant/components/renault/translations/es.json @@ -1,18 +1,26 @@ { "config": { "abort": { - "already_configured": "La cuenta ya est\u00e1 configurada" + "already_configured": "La cuenta ya est\u00e1 configurada", + "kamereon_no_account": "No se pudo encontrar la cuenta de Kamereon." }, "error": { "invalid_credentials": "Autenticaci\u00f3n err\u00f3nea" }, "step": { + "kamereon": { + "data": { + "kamereon_account_id": "ID de cuenta de Kamereon" + }, + "title": "Seleccione el id de la cuenta de Kamereon" + }, "user": { "data": { "locale": "Configuraci\u00f3n regional", "password": "Clave", "username": "Correo-e" - } + }, + "title": "Establecer las credenciales de Renault" } } } diff --git a/homeassistant/components/renault/translations/zh-Hans.json b/homeassistant/components/renault/translations/zh-Hans.json index b081f64a961..ab8c60ed030 100644 --- a/homeassistant/components/renault/translations/zh-Hans.json +++ b/homeassistant/components/renault/translations/zh-Hans.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "locale": "\u5730\u533a", "password": "\u5bc6\u7801", "username": "\u7535\u5b50\u90ae\u7bb1" }, diff --git a/homeassistant/components/rituals_perfume_genie/translations/zh-Hans.json b/homeassistant/components/rituals_perfume_genie/translations/zh-Hans.json new file mode 100644 index 00000000000..d217ccdc842 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/zh-Hans.json b/homeassistant/components/roomba/translations/zh-Hans.json new file mode 100644 index 00000000000..7674a49c492 --- /dev/null +++ b/homeassistant/components/roomba/translations/zh-Hans.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "not_irobot_device": "\u5df2\u53d1\u73b0\u7684\u8bbe\u5907\u5e76\u4e0d\u662f iRobot \u8bbe\u5907" + }, + "step": { + "manual": { + "description": "\u672a\u5728\u60a8\u7684\u7f51\u7edc\u4e0a\u53d1\u73b0 Roomba \u6216 Braava\u3002 BLID \u662f\u8bbe\u5907\u4e3b\u673a\u540d\u4e2d \u201ciRobot-\u201d \u6216 \u201cRoomba-\u201d \u4e4b\u540e\u7684\u90e8\u5206\u3002\u8bf7\u6309\u7167\u6587\u6863\u4e2d\u6982\u8ff0\u7684\u6b65\u9aa4\u64cd\u4f5c\uff1a {auth_help_url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/en.json b/homeassistant/components/sensor/translations/en.json index f8f45f93309..69737c7c93a 100644 --- a/homeassistant/components/sensor/translations/en.json +++ b/homeassistant/components/sensor/translations/en.json @@ -6,6 +6,7 @@ "is_carbon_monoxide": "Current {entity_name} carbon monoxide concentration level", "is_current": "Current {entity_name} current", "is_energy": "Current {entity_name} energy", + "is_gas": "Current {entity_name} gas", "is_humidity": "Current {entity_name} humidity", "is_illuminance": "Current {entity_name} illuminance", "is_power": "Current {entity_name} power", @@ -22,6 +23,7 @@ "carbon_monoxide": "{entity_name} carbon monoxide concentration changes", "current": "{entity_name} current changes", "energy": "{entity_name} energy changes", + "gas": "{entity_name} gas changes", "humidity": "{entity_name} humidity changes", "illuminance": "{entity_name} illuminance changes", "power": "{entity_name} power changes", diff --git a/homeassistant/components/sensor/translations/et.json b/homeassistant/components/sensor/translations/et.json index 4169e7b82db..839f505f6aa 100644 --- a/homeassistant/components/sensor/translations/et.json +++ b/homeassistant/components/sensor/translations/et.json @@ -6,6 +6,7 @@ "is_carbon_monoxide": "{entity_name} praegune vingugaasi tase", "is_current": "Praegune {entity_name} voolutugevus", "is_energy": "Praegune {entity_name} v\u00f5imsus", + "is_gas": "Praegune {entity_name} gaas", "is_humidity": "Praegune {entity_name} niiskus", "is_illuminance": "Praegune {entity_name} valgustatus", "is_power": "Praegune {entity_name} toide (v\u00f5imsus)", @@ -22,6 +23,7 @@ "carbon_monoxide": "{entity_name} vingugaasi tase muutus", "current": "{entity_name} voolutugevus muutub", "energy": "{entity_name} v\u00f5imsus muutub", + "gas": "{entity_name} gaasivahetus", "humidity": "{entity_name} niiskus muutub", "illuminance": "{entity_name} valgustustugevus muutub", "power": "{entity_name} energiare\u017eiimi muutub", diff --git a/homeassistant/components/sensor/translations/zh-Hant.json b/homeassistant/components/sensor/translations/zh-Hant.json index a15af383da6..b22ba82f3a4 100644 --- a/homeassistant/components/sensor/translations/zh-Hant.json +++ b/homeassistant/components/sensor/translations/zh-Hant.json @@ -6,6 +6,7 @@ "is_carbon_monoxide": "\u76ee\u524d {entity_name} \u4e00\u6c27\u5316\u78b3\u6fc3\u5ea6\u72c0\u614b", "is_current": "\u76ee\u524d{entity_name}\u96fb\u6d41", "is_energy": "\u76ee\u524d{entity_name}\u96fb\u529b", + "is_gas": "\u76ee\u524d{entity_name}\u6c23\u9ad4", "is_humidity": "\u76ee\u524d{entity_name}\u6fd5\u5ea6", "is_illuminance": "\u76ee\u524d{entity_name}\u7167\u5ea6", "is_power": "\u76ee\u524d{entity_name}\u96fb\u529b", @@ -22,6 +23,7 @@ "carbon_monoxide": "{entity_name} \u4e00\u6c27\u5316\u78b3\u6fc3\u5ea6\u8b8a\u5316", "current": "\u76ee\u524d{entity_name}\u96fb\u6d41\u8b8a\u66f4", "energy": "\u76ee\u524d{entity_name}\u96fb\u529b\u8b8a\u66f4", + "gas": "{entity_name}\u6c23\u9ad4\u8b8a\u66f4", "humidity": "{entity_name}\u6fd5\u5ea6\u8b8a\u66f4", "illuminance": "{entity_name}\u7167\u5ea6\u8b8a\u66f4", "power": "{entity_name}\u96fb\u529b\u8b8a\u66f4", diff --git a/homeassistant/components/shopping_list/translations/zh-Hans.json b/homeassistant/components/shopping_list/translations/zh-Hans.json new file mode 100644 index 00000000000..fa498b4ff60 --- /dev/null +++ b/homeassistant/components/shopping_list/translations/zh-Hans.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52a1\u5df2\u914d\u7f6e" + }, + "step": { + "user": { + "description": "\u60a8\u8981\u914d\u7f6e\u8d2d\u7269\u6e05\u5355\u5417\uff1f", + "title": "\u8d2d\u7269\u6e05\u5355" + } + } + }, + "title": "\u8d2d\u7269\u6e05\u5355" +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/translations/zh-Hans.json b/homeassistant/components/smartthings/translations/zh-Hans.json index 849d69d55e5..3db5d7f0354 100644 --- a/homeassistant/components/smartthings/translations/zh-Hans.json +++ b/homeassistant/components/smartthings/translations/zh-Hans.json @@ -8,6 +8,11 @@ "webhook_error": "SmartThings \u65e0\u6cd5\u9a8c\u8bc1 `base_url` \u4e2d\u914d\u7f6e\u7684\u7aef\u70b9\u3002\u8bf7\u67e5\u770b\u7ec4\u4ef6\u9700\u6c42\u3002" }, "step": { + "pat": { + "data": { + "access_token": "\u8bbf\u95ee\u4ee4\u724c" + } + }, "user": { "description": "\u8bf7\u8f93\u5165\u6309\u7167[\u8bf4\u660e]({component_url})\u521b\u5efa\u7684 SmartThings [\u4e2a\u4eba\u8bbf\u95ee\u4ee4\u724c]({token_url})\u3002", "title": "\u8f93\u5165\u4e2a\u4eba\u8bbf\u95ee\u4ee4\u724c" diff --git a/homeassistant/components/smarttub/translations/zh-Hans.json b/homeassistant/components/smarttub/translations/zh-Hans.json new file mode 100644 index 00000000000..d217ccdc842 --- /dev/null +++ b/homeassistant/components/smarttub/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/translations/zh-Hans.json b/homeassistant/components/soma/translations/zh-Hans.json new file mode 100644 index 00000000000..51fbc254b7f --- /dev/null +++ b/homeassistant/components/soma/translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "connection_error": "\u65e0\u6cd5\u8fde\u63a5 SOMA Connect\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/zh-Hans.json b/homeassistant/components/sonarr/translations/zh-Hans.json new file mode 100644 index 00000000000..265928213f5 --- /dev/null +++ b/homeassistant/components/sonarr/translations/zh-Hans.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52a1\u5df2\u88ab\u914d\u7f6e", + "reauth_successful": "\u91cd\u9a8c\u8bc1\u6210\u529f", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u9a8c\u8bc1\u65e0\u6548" + }, + "flow_title": "{name}", + "step": { + "reauth_confirm": { + "description": "Sonarr \u96c6\u6210\u9700\u8981\u624b\u52a8\u91cd\u65b0\u9a8c\u8bc1\uff1a{host}" + }, + "user": { + "data": { + "api_key": "API \u5bc6\u94a5", + "host": "\u4e3b\u673a\u5730\u5740", + "port": "\u7aef\u53e3", + "ssl": "\u4f7f\u7528 SSL \u8bc1\u4e66", + "verify_ssl": "\u9a8c\u8bc1 SSL \u8bc1\u4e66" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "\u663e\u793a\u5373\u5c06\u5185\u5bb9\u7684\u5929\u6570", + "wanted_max_items": "\u5185\u5bb9\u663e\u793a\u6700\u5927\u6570\u91cf" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/zh-Hans.json b/homeassistant/components/srp_energy/translations/zh-Hans.json new file mode 100644 index 00000000000..36016f3e217 --- /dev/null +++ b/homeassistant/components/srp_energy/translations/zh-Hans.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801", + "username": "\u7528\u6237\u540d" + } + } + } + }, + "title": "SRP Energy" +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/zh-Hans.json b/homeassistant/components/subaru/translations/zh-Hans.json new file mode 100644 index 00000000000..d217ccdc842 --- /dev/null +++ b/homeassistant/components/subaru/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/translations/es.json b/homeassistant/components/tesla/translations/es.json index 54fbfd1a21d..8211e806741 100644 --- a/homeassistant/components/tesla/translations/es.json +++ b/homeassistant/components/tesla/translations/es.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "mfa": "C\u00f3digo MFA (opcional)", "password": "Contrase\u00f1a", "username": "Correo electr\u00f3nico" }, diff --git a/homeassistant/components/tesla/translations/zh-Hans.json b/homeassistant/components/tesla/translations/zh-Hans.json new file mode 100644 index 00000000000..35635ce3be3 --- /dev/null +++ b/homeassistant/components/tesla/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "mfa": "MFA \u4ee3\u7801\uff08\u53ef\u9009\uff09" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/es-419.json b/homeassistant/components/uptimerobot/translations/es-419.json new file mode 100644 index 00000000000..445247107c0 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/es-419.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/es.json b/homeassistant/components/uptimerobot/translations/es.json index 1f88050745d..d3c7f2b036d 100644 --- a/homeassistant/components/uptimerobot/translations/es.json +++ b/homeassistant/components/uptimerobot/translations/es.json @@ -2,18 +2,29 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", + "reauth_failed_existing": "No se pudo actualizar la entrada de configuraci\u00f3n, elimine la integraci\u00f3n y config\u00farela nuevamente.", + "reauth_successful": "La reautenticaci\u00f3n fue exitosa", "unknown": "Error desconocido" }, "error": { "cannot_connect": "No se pudo conectar", "invalid_api_key": "Clave de la API err\u00f3nea", + "reauth_failed_matching_account": "La clave de API que proporcion\u00f3 no coincide con el ID de cuenta para la configuraci\u00f3n existente.", "unknown": "Error desconocido" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API Key" + }, + "description": "Debe proporcionar una nueva clave API de solo lectura de Uptime Robot", + "title": "Volver a autenticar la integraci\u00f3n" + }, "user": { "data": { "api_key": "Clave de la API" - } + }, + "description": "Debe proporcionar una clave API de solo lectura de robot de tiempo de actividad/funcionamiento" } } } diff --git a/homeassistant/components/uptimerobot/translations/zh-Hans.json b/homeassistant/components/uptimerobot/translations/zh-Hans.json index 92106b06ce2..d680c09e967 100644 --- a/homeassistant/components/uptimerobot/translations/zh-Hans.json +++ b/homeassistant/components/uptimerobot/translations/zh-Hans.json @@ -2,18 +2,28 @@ "config": { "abort": { "already_configured": "\u6b64\u8d26\u53f7\u5df2\u88ab\u914d\u7f6e", + "reauth_failed_existing": "\u65e0\u6cd5\u66f4\u65b0\u914d\u7f6e\u6761\u76ee\uff0c\u8bf7\u5220\u9664\u96c6\u6210\u5e76\u91cd\u65b0\u8bbe\u7f6e\u3002", + "reauth_successful": "\u91cd\u9a8c\u8bc1\u6210\u529f", "unknown": "\u672a\u77e5\u9519\u8bef" }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", "invalid_api_key": "\u65e0\u6548\u7684 API \u5bc6\u94a5", + "reauth_failed_matching_account": "\u60a8\u63d0\u4f9b\u7684 API \u5bc6\u94a5\u4e0e\u73b0\u6709\u914d\u7f6e\u7684\u8d26\u53f7 ID \u4e0d\u5339\u914d", "unknown": "\u672a\u77e5\u9519\u8bef" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u5bc6\u94a5" + }, + "description": "\u60a8\u9700\u8981\u4ece Uptime Robot \u4e2d\u63d0\u4f9b\u4e00\u4e2a\"\u53ea\u8bfb API \u5bc6\u94a5\"" + }, "user": { "data": { "api_key": "API \u5bc6\u94a5" - } + }, + "description": "\u60a8\u9700\u8981\u4ece Uptime Robot \u4e2d\u63d0\u4f9b\u4e00\u4e2a\"\u53ea\u8bfb API \u5bc6\u94a5\"" } } } diff --git a/homeassistant/components/verisure/translations/zh-Hans.json b/homeassistant/components/verisure/translations/zh-Hans.json new file mode 100644 index 00000000000..e786edb1405 --- /dev/null +++ b/homeassistant/components/verisure/translations/zh-Hans.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u7801" + } + }, + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/zh-Hans.json b/homeassistant/components/wallbox/translations/zh-Hans.json new file mode 100644 index 00000000000..d217ccdc842 --- /dev/null +++ b/homeassistant/components/wallbox/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801" + } + } + } + } +} \ 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 0034f73fbf3..c3eb4affc4c 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hans.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hans.json @@ -56,7 +56,8 @@ "data": { "host": "IP \u5730\u5740", "token": "API Token" - } + }, + "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" diff --git a/homeassistant/components/yale_smart_alarm/translations/es-419.json b/homeassistant/components/yale_smart_alarm/translations/es-419.json new file mode 100644 index 00000000000..f3cbae5ed03 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/es-419.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "area_id": "ID de \u00c1rea" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/zh-Hans.json b/homeassistant/components/yale_smart_alarm/translations/zh-Hans.json new file mode 100644 index 00000000000..2afd7efdb15 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/zh-Hans.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u8d26\u53f7\u5df2\u88ab\u914d\u7f6e" + }, + "error": { + "invalid_auth": "\u9a8c\u8bc1\u65e0\u6548" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "\u533a\u57df ID", + "name": "\u540d\u79f0", + "password": "\u5bc6\u7801", + "username": "\u7528\u6237\u540d" + } + }, + "user": { + "data": { + "area_id": "\u533a\u57df ID", + "name": "\u540d\u79f0", + "password": "\u5bc6\u7801", + "username": "\u8d26\u53f7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/zh-Hans.json b/homeassistant/components/yamaha_musiccast/translations/zh-Hans.json new file mode 100644 index 00000000000..f69ab4546d5 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/zh-Hans.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "\u8bbe\u7f6e MusicCast \u4ee5\u4e0e Home Assistant \u96c6\u6210\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/zh-Hans.json b/homeassistant/components/yeelight/translations/zh-Hans.json new file mode 100644 index 00000000000..43fb1d9fe25 --- /dev/null +++ b/homeassistant/components/yeelight/translations/zh-Hans.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", + "no_devices_found": "\u60a8\u7684\u7f51\u7edc\u672a\u53d1\u73b0 Yeelight \u8bbe\u5907" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + }, + "flow_title": "{model} {host}", + "step": { + "discovery_confirm": { + "description": "\u60a8\u8981\u8bbe\u7f6e {model} ( {host} )\u5417\uff1f" + }, + "pick_device": { + "data": { + "device": "\u8bbe\u5907" + } + }, + "user": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740" + }, + "description": "\u5982\u679c\u60a8\u5c06\u4e3b\u673a\u5730\u5740\u680f\u7559\u7a7a\uff0c\u5219\u5c06\u81ea\u52a8\u5bfb\u627e\u8bbe\u5907\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "\u578b\u53f7\uff08\u53ef\u9009\uff09", + "nightlight_switch": "\u4f7f\u7528\u591c\u5149\u5f00\u5173", + "save_on_change": "\u4fdd\u5b58\u66f4\u6539\u72b6\u6001", + "transition": "\u8fc7\u6e21\u65f6\u95f4\uff08\u6beb\u79d2\uff09", + "use_music_mode": "\u542f\u7528\u97f3\u4e50\u6a21\u5f0f" + }, + "description": "\u5982\u679c\u5c06\u4fe1\u53f7\u680f\u7559\u7a7a\uff0c\u96c6\u6210\u5c06\u4f1a\u81ea\u52a8\u68c0\u6d4b\u76f8\u5173\u4fe1\u606f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/zh-Hans.json b/homeassistant/components/youless/translations/zh-Hans.json new file mode 100644 index 00000000000..cfe90f18df3 --- /dev/null +++ b/homeassistant/components/youless/translations/zh-Hans.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "name": "\u540d\u79f0" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/zh-Hans.json b/homeassistant/components/zoneminder/translations/zh-Hans.json index a5f4ff11f09..8f3265d4344 100644 --- a/homeassistant/components/zoneminder/translations/zh-Hans.json +++ b/homeassistant/components/zoneminder/translations/zh-Hans.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "connection_error": "\u65e0\u6cd5\u8fde\u63a5\u81f3 ZoneMinder \u670d\u52a1\u5668\u3002" + }, + "error": { + "connection_error": "\u65e0\u6cd5\u8fde\u63a5\u81f3 ZoneMinder \u670d\u52a1\u5668\u3002" + }, "step": { "user": { "data": { From c040be423a20d4e21ba068c0bb59695509caaf48 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Wed, 11 Aug 2021 21:17:25 -0600 Subject: [PATCH 170/355] Updates to bump MyQ to 3.1.2 (#54488) --- .coveragerc | 1 + homeassistant/components/myq/config_flow.py | 2 +- homeassistant/components/myq/cover.py | 8 ++++++-- homeassistant/components/myq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.coveragerc b/.coveragerc index bf51d5ad594..3795f7e49b8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -671,6 +671,7 @@ omit = homeassistant/components/mystrom/light.py homeassistant/components/mystrom/switch.py homeassistant/components/myq/__init__.py + homeassistant/components/myq/cover.py homeassistant/components/nad/media_player.py homeassistant/components/nanoleaf/light.py homeassistant/components/neato/__init__.py diff --git a/homeassistant/components/myq/config_flow.py b/homeassistant/components/myq/config_flow.py index 78a751a18b1..8c088de6715 100644 --- a/homeassistant/components/myq/config_flow.py +++ b/homeassistant/components/myq/config_flow.py @@ -31,7 +31,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Validate the user input allows us to connect.""" websession = aiohttp_client.async_get_clientsession(self.hass) try: - await pymyq.login(username, password, websession) + await pymyq.login(username, password, websession, True) except InvalidCredentialsError: return {CONF_PASSWORD: "invalid_auth"} except MyQError: diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index 3d587635f2d..8d36db8e0ab 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -115,7 +115,9 @@ class MyQDevice(CoordinatorEntity, CoverEntity): # Write closing state to HASS self.async_write_ha_state() - if not await wait_task: + result = wait_task if isinstance(wait_task, bool) else await wait_task + + if not result: _LOGGER.error("Closing of cover %s failed", self._device.name) # Write final state to HASS @@ -137,7 +139,9 @@ class MyQDevice(CoordinatorEntity, CoverEntity): # Write opening state to HASS self.async_write_ha_state() - if not await wait_task: + result = wait_task if isinstance(wait_task, bool) else await wait_task + + if not result: _LOGGER.error("Opening of cover %s failed", self._device.name) # Write final state to HASS diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index a93501c941f..a4de12290f1 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -2,7 +2,7 @@ "domain": "myq", "name": "MyQ", "documentation": "https://www.home-assistant.io/integrations/myq", - "requirements": ["pymyq==3.0.4"], + "requirements": ["pymyq==3.1.2"], "codeowners": ["@bdraco"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index bbf9e8344f2..76a9b4f7543 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1610,7 +1610,7 @@ pymonoprice==0.3 pymsteams==0.1.12 # homeassistant.components.myq -pymyq==3.0.4 +pymyq==3.1.2 # homeassistant.components.mysensors pymysensors==0.21.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf1c915d751..988c2bfb2c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -920,7 +920,7 @@ pymodbus==2.5.3rc1 pymonoprice==0.3 # homeassistant.components.myq -pymyq==3.0.4 +pymyq==3.1.2 # homeassistant.components.mysensors pymysensors==0.21.0 From 49a69d5ba093c3ad6d95667632e866b44f9f8adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 12 Aug 2021 07:30:35 +0200 Subject: [PATCH 171/355] Add missing PRESSURE_BAR conversion (#54497) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add missing PRESSURE_BAR * style Signed-off-by: Daniel Hjelseth Høyer * valid units Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/util/pressure.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/util/pressure.py b/homeassistant/util/pressure.py index 188cf66491e..95b32a69643 100644 --- a/homeassistant/util/pressure.py +++ b/homeassistant/util/pressure.py @@ -5,6 +5,7 @@ from numbers import Number from homeassistant.const import ( PRESSURE, + PRESSURE_BAR, PRESSURE_HPA, PRESSURE_INHG, PRESSURE_MBAR, @@ -16,6 +17,7 @@ from homeassistant.const import ( VALID_UNITS: tuple[str, ...] = ( PRESSURE_PA, PRESSURE_HPA, + PRESSURE_BAR, PRESSURE_MBAR, PRESSURE_INHG, PRESSURE_PSI, @@ -24,6 +26,7 @@ VALID_UNITS: tuple[str, ...] = ( UNIT_CONVERSION: dict[str, float] = { PRESSURE_PA: 1, PRESSURE_HPA: 1 / 100, + PRESSURE_BAR: 1 / 100000, PRESSURE_MBAR: 1 / 100, PRESSURE_INHG: 1 / 3386.389, PRESSURE_PSI: 1 / 6894.757, From e55868b17f22c089ed38fec7748b279dddefc41e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 12 Aug 2021 12:38:33 +0200 Subject: [PATCH 172/355] Use entity class attributes for Adax (#54501) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adax attributes Signed-off-by: Daniel Hjelseth Høyer * Adax attributes Signed-off-by: Daniel Hjelseth Høyer * Update homeassistant/components/adax/climate.py Co-authored-by: Joakim Sørensen * style Signed-off-by: Daniel Hjelseth Høyer Co-authored-by: Joakim Sørensen --- homeassistant/components/adax/climate.py | 42 +++++------------------- 1 file changed, 8 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/adax/climate.py b/homeassistant/components/adax/climate.py index 74e973ba6d5..1abd83fdbfc 100644 --- a/homeassistant/components/adax/climate.py +++ b/homeassistant/components/adax/climate.py @@ -49,20 +49,19 @@ async def async_setup_entry( class AdaxDevice(ClimateEntity): """Representation of a heater.""" + _attr_hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_OFF] + _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, heater_data: dict[str, Any], adax_data_handler: Adax) -> None: """Initialize the heater.""" self._heater_data = heater_data self._adax_data_handler = adax_data_handler - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return SUPPORT_TARGET_TEMPERATURE - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return f"{self._heater_data['homeId']}_{self._heater_data['id']}" + self._attr_unique_id = f"{heater_data['homeId']}_{heater_data['id']}" @property def name(self) -> str: @@ -83,11 +82,6 @@ class AdaxDevice(ClimateEntity): return "mdi:radiator" return "mdi:radiator-off" - @property - def hvac_modes(self) -> list[str]: - """Return the list of available hvac operation modes.""" - return [HVAC_MODE_HEAT, HVAC_MODE_OFF] - async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set hvac mode.""" if hvac_mode == HVAC_MODE_HEAT: @@ -105,21 +99,6 @@ class AdaxDevice(ClimateEntity): return await self._adax_data_handler.update() - @property - def temperature_unit(self) -> str: - """Return the unit of measurement which this device uses.""" - return TEMP_CELSIUS - - @property - def min_temp(self) -> int: - """Return the minimum temperature.""" - return 5 - - @property - def max_temp(self) -> int: - """Return the maximum temperature.""" - return 35 - @property def current_temperature(self) -> float | None: """Return the current temperature.""" @@ -130,11 +109,6 @@ class AdaxDevice(ClimateEntity): """Return the temperature we try to reach.""" return self._heater_data.get("targetTemperature") - @property - def target_temperature_step(self) -> int: - """Return the supported step of target temperature.""" - return PRECISION_WHOLE - async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) From 103e21c278300d328b1b5bbc78d9d6fd77908b6c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 12 Aug 2021 13:26:17 +0200 Subject: [PATCH 173/355] Move temperature conversions to sensor base class (5/8) (#54475) --- homeassistant/components/nam/const.py | 38 ++--- homeassistant/components/nam/sensor.py | 4 +- homeassistant/components/neato/sensor.py | 4 +- .../nederlandse_spoorwegen/sensor.py | 2 +- .../components/nest/legacy/sensor.py | 8 +- homeassistant/components/nest/sensor_sdm.py | 8 +- homeassistant/components/netatmo/sensor.py | 54 +++---- homeassistant/components/netdata/sensor.py | 6 +- .../components/netgear_lte/sensor.py | 10 +- .../components/neurio_energy/sensor.py | 4 +- homeassistant/components/nexia/sensor.py | 8 +- homeassistant/components/nextbus/sensor.py | 2 +- homeassistant/components/nextcloud/sensor.py | 2 +- homeassistant/components/nightscout/sensor.py | 4 +- .../components/nissan_leaf/sensor.py | 8 +- homeassistant/components/nmbs/sensor.py | 6 +- homeassistant/components/noaa_tides/sensor.py | 2 +- homeassistant/components/notion/sensor.py | 4 +- .../components/nsw_fuel_station/sensor.py | 4 +- homeassistant/components/numato/sensor.py | 4 +- homeassistant/components/nut/const.py | 138 +++++++++--------- homeassistant/components/nut/sensor.py | 2 +- homeassistant/components/nws/const.py | 22 +-- homeassistant/components/nws/sensor.py | 6 +- homeassistant/components/nzbget/sensor.py | 4 +- .../components/oasa_telematics/sensor.py | 2 +- homeassistant/components/obihai/sensor.py | 2 +- homeassistant/components/octoprint/sensor.py | 4 +- homeassistant/components/ohmconnect/sensor.py | 2 +- homeassistant/components/ombi/sensor.py | 2 +- homeassistant/components/omnilogic/sensor.py | 14 +- homeassistant/components/ondilo_ico/sensor.py | 16 +- homeassistant/components/onewire/sensor.py | 6 +- homeassistant/components/onvif/sensor.py | 4 +- homeassistant/components/openerz/sensor.py | 2 +- homeassistant/components/openevse/sensor.py | 4 +- .../components/openexchangerates/sensor.py | 2 +- .../components/openhardwaremonitor/sensor.py | 4 +- homeassistant/components/opensky/sensor.py | 4 +- .../components/opentherm_gw/sensor.py | 4 +- homeassistant/components/openuv/sensor.py | 20 +-- .../openweathermap/abstract_owm_sensor.py | 2 +- .../components/openweathermap/sensor.py | 4 +- homeassistant/components/oru/sensor.py | 4 +- homeassistant/components/otp/sensor.py | 2 +- homeassistant/components/ovo_energy/sensor.py | 10 +- homeassistant/components/ozw/sensor.py | 10 +- homeassistant/components/pi_hole/const.py | 18 +-- homeassistant/components/pi_hole/sensor.py | 2 +- homeassistant/components/picnic/sensor.py | 7 +- homeassistant/components/pilight/sensor.py | 4 +- homeassistant/components/plaato/sensor.py | 4 +- homeassistant/components/plex/sensor.py | 8 +- homeassistant/components/plugwise/sensor.py | 4 +- .../components/pocketcasts/sensor.py | 2 +- homeassistant/components/point/sensor.py | 4 +- homeassistant/components/poolsense/sensor.py | 4 +- homeassistant/components/powerwall/sensor.py | 8 +- homeassistant/components/pushbullet/sensor.py | 2 +- homeassistant/components/pvoutput/sensor.py | 4 +- .../components/pvpc_hourly_pricing/sensor.py | 2 +- homeassistant/components/pyload/sensor.py | 4 +- .../components/qbittorrent/sensor.py | 4 +- homeassistant/components/qnap/sensor.py | 14 +- homeassistant/components/qwikswitch/sensor.py | 4 +- 65 files changed, 289 insertions(+), 288 deletions(-) diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py index 85472deba06..a9d044f2c1d 100644 --- a/homeassistant/components/nam/const.py +++ b/homeassistant/components/nam/const.py @@ -65,133 +65,133 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( SensorEntityDescription( key=ATTR_BME280_HUMIDITY, name=f"{DEFAULT_NAME} BME280 Humidity", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_BME280_PRESSURE, name=f"{DEFAULT_NAME} BME280 Pressure", - unit_of_measurement=PRESSURE_HPA, + native_unit_of_measurement=PRESSURE_HPA, device_class=DEVICE_CLASS_PRESSURE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_BME280_TEMPERATURE, name=f"{DEFAULT_NAME} BME280 Temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_BMP280_PRESSURE, name=f"{DEFAULT_NAME} BMP280 Pressure", - unit_of_measurement=PRESSURE_HPA, + native_unit_of_measurement=PRESSURE_HPA, device_class=DEVICE_CLASS_PRESSURE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_BMP280_TEMPERATURE, name=f"{DEFAULT_NAME} BMP280 Temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_HECA_HUMIDITY, name=f"{DEFAULT_NAME} HECA Humidity", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_HECA_TEMPERATURE, name=f"{DEFAULT_NAME} HECA Temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_MHZ14A_CARBON_DIOXIDE, name=f"{DEFAULT_NAME} MH-Z14A Carbon Dioxide", - unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, device_class=DEVICE_CLASS_CO2, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_SDS011_P1, name=f"{DEFAULT_NAME} SDS011 Particulate Matter 10", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, icon="mdi:blur", state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_SDS011_P2, name=f"{DEFAULT_NAME} SDS011 Particulate Matter 2.5", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, icon="mdi:blur", state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_SHT3X_HUMIDITY, name=f"{DEFAULT_NAME} SHT3X Humidity", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_SHT3X_TEMPERATURE, name=f"{DEFAULT_NAME} SHT3X Temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_SPS30_P0, name=f"{DEFAULT_NAME} SPS30 Particulate Matter 1.0", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, icon="mdi:blur", state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_SPS30_P1, name=f"{DEFAULT_NAME} SPS30 Particulate Matter 10", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, icon="mdi:blur", state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_SPS30_P2, name=f"{DEFAULT_NAME} SPS30 Particulate Matter 2.5", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, icon="mdi:blur", state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_SPS30_P4, name=f"{DEFAULT_NAME} SPS30 Particulate Matter 4.0", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, icon="mdi:blur", state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_DHT22_HUMIDITY, name=f"{DEFAULT_NAME} DHT22 Humidity", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_DHT22_TEMPERATURE, name=f"{DEFAULT_NAME} DHT22 Temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_SIGNAL_STRENGTH, name=f"{DEFAULT_NAME} Signal Strength", - unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, entity_registry_enabled_default=False, state_class=STATE_CLASS_MEASUREMENT, diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index 298f88d5c29..c5c9c9f2e77 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -75,7 +75,7 @@ class NAMSensor(CoordinatorEntity, SensorEntity): self.entity_description = description @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state.""" return cast( StateType, getattr(self.coordinator.data, self.entity_description.key) @@ -99,7 +99,7 @@ class NAMSensorUptime(NAMSensor): """Define an Nettigo Air Monitor uptime sensor.""" @property - def state(self) -> str: + def native_value(self) -> str: """Return the state.""" uptime_sec = getattr(self.coordinator.data, self.entity_description.key) return ( diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 1cf10112b92..2d54e89bb04 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -89,14 +89,14 @@ class NeatoSensor(SensorEntity): return self._available @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state.""" if self._state is not None: return str(self._state["details"]["charge"]) return None @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return unit of measurement.""" return PERCENTAGE diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index de8a85f44fd..8cbe7b1f803 100644 --- a/homeassistant/components/nederlandse_spoorwegen/sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py @@ -118,7 +118,7 @@ class NSDepartureSensor(SensorEntity): return ICON @property - def state(self): + def native_value(self): """Return the next departure time.""" return self._state diff --git a/homeassistant/components/nest/legacy/sensor.py b/homeassistant/components/nest/legacy/sensor.py index 0939e925b43..f2c6670bf8b 100644 --- a/homeassistant/components/nest/legacy/sensor.py +++ b/homeassistant/components/nest/legacy/sensor.py @@ -154,12 +154,12 @@ class NestBasicSensor(NestSensorDevice, SensorEntity): """Representation a basic Nest sensor.""" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -189,12 +189,12 @@ class NestTempSensor(NestSensorDevice, SensorEntity): """Representation of a Nest Temperature sensor.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index 42614af8c40..0034acff3af 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -95,13 +95,13 @@ class TemperatureSensor(SensorBase): return f"{self._device_info.device_name} Temperature" @property - def state(self) -> float: + def native_value(self) -> float: """Return the state of the sensor.""" trait: TemperatureTrait = self._device.traits[TemperatureTrait.NAME] return trait.ambient_temperature_celsius @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit of measurement.""" return TEMP_CELSIUS @@ -126,13 +126,13 @@ class HumiditySensor(SensorBase): return f"{self._device_info.device_name} Humidity" @property - def state(self) -> float: + def native_value(self) -> float: """Return the state of the sensor.""" trait: HumidityTrait = self._device.traits[HumidityTrait.NAME] return trait.ambient_humidity_percent @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit of measurement.""" return PERCENTAGE diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 0c55b459847..a1f7b2ac079 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -83,7 +83,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Temperature", netatmo_name="Temperature", entity_registry_enabled_default=True, - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), @@ -98,7 +98,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( key="co2", name="CO2", netatmo_name="CO2", - unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, entity_registry_enabled_default=True, device_class=DEVICE_CLASS_CO2, state_class=STATE_CLASS_MEASUREMENT, @@ -108,7 +108,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Pressure", netatmo_name="Pressure", entity_registry_enabled_default=True, - unit_of_measurement=PRESSURE_MBAR, + native_unit_of_measurement=PRESSURE_MBAR, device_class=DEVICE_CLASS_PRESSURE, state_class=STATE_CLASS_MEASUREMENT, ), @@ -124,7 +124,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Noise", netatmo_name="Noise", entity_registry_enabled_default=True, - unit_of_measurement=SOUND_PRESSURE_DB, + native_unit_of_measurement=SOUND_PRESSURE_DB, icon="mdi:volume-high", state_class=STATE_CLASS_MEASUREMENT, ), @@ -133,7 +133,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Humidity", netatmo_name="Humidity", entity_registry_enabled_default=True, - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), @@ -142,7 +142,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Rain", netatmo_name="Rain", entity_registry_enabled_default=True, - unit_of_measurement=LENGTH_MILLIMETERS, + native_unit_of_measurement=LENGTH_MILLIMETERS, icon="mdi:weather-rainy", ), NetatmoSensorEntityDescription( @@ -150,7 +150,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Rain last hour", netatmo_name="sum_rain_1", entity_registry_enabled_default=False, - unit_of_measurement=LENGTH_MILLIMETERS, + native_unit_of_measurement=LENGTH_MILLIMETERS, icon="mdi:weather-rainy", ), NetatmoSensorEntityDescription( @@ -158,7 +158,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Rain today", netatmo_name="sum_rain_24", entity_registry_enabled_default=True, - unit_of_measurement=LENGTH_MILLIMETERS, + native_unit_of_measurement=LENGTH_MILLIMETERS, icon="mdi:weather-rainy", ), NetatmoSensorEntityDescription( @@ -166,7 +166,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Battery Percent", netatmo_name="battery_percent", entity_registry_enabled_default=True, - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_BATTERY, state_class=STATE_CLASS_MEASUREMENT, ), @@ -182,7 +182,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Angle", netatmo_name="WindAngle", entity_registry_enabled_default=False, - unit_of_measurement=DEGREE, + native_unit_of_measurement=DEGREE, icon="mdi:compass-outline", state_class=STATE_CLASS_MEASUREMENT, ), @@ -191,7 +191,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Wind Strength", netatmo_name="WindStrength", entity_registry_enabled_default=True, - unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, icon="mdi:weather-windy", state_class=STATE_CLASS_MEASUREMENT, ), @@ -207,7 +207,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Gust Angle", netatmo_name="GustAngle", entity_registry_enabled_default=False, - unit_of_measurement=DEGREE, + native_unit_of_measurement=DEGREE, icon="mdi:compass-outline", state_class=STATE_CLASS_MEASUREMENT, ), @@ -216,7 +216,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Gust Strength", netatmo_name="GustStrength", entity_registry_enabled_default=False, - unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, icon="mdi:weather-windy", state_class=STATE_CLASS_MEASUREMENT, ), @@ -239,7 +239,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Radio Level", netatmo_name="rf_status", entity_registry_enabled_default=False, - unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, state_class=STATE_CLASS_MEASUREMENT, ), @@ -255,7 +255,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Wifi Level", netatmo_name="wifi_status", entity_registry_enabled_default=False, - unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, state_class=STATE_CLASS_MEASUREMENT, ), @@ -518,25 +518,25 @@ class NetatmoSensor(NetatmoBase, SensorEntity): self._device_name, self._id, ) - self._attr_state = None + self._attr_native_value = None return try: state = data[self.entity_description.netatmo_name] if self.entity_description.key in {"temperature", "pressure", "sum_rain_1"}: - self._attr_state = round(state, 1) + self._attr_native_value = round(state, 1) elif self.entity_description.key in {"windangle_value", "gustangle_value"}: - self._attr_state = fix_angle(state) + self._attr_native_value = fix_angle(state) elif self.entity_description.key in {"windangle", "gustangle"}: - self._attr_state = process_angle(fix_angle(state)) + self._attr_native_value = process_angle(fix_angle(state)) elif self.entity_description.key == "rf_status": - self._attr_state = process_rf(state) + self._attr_native_value = process_rf(state) elif self.entity_description.key == "wifi_status": - self._attr_state = process_wifi(state) + self._attr_native_value = process_wifi(state) elif self.entity_description.key == "health_idx": - self._attr_state = process_health(state) + self._attr_native_value = process_health(state) else: - self._attr_state = state + self._attr_native_value = state except KeyError: if self.state: _LOGGER.debug( @@ -544,7 +544,7 @@ class NetatmoSensor(NetatmoBase, SensorEntity): self.entity_description.key, self._device_name, ) - self._attr_state = None + self._attr_native_value = None return self.async_write_ha_state() @@ -758,14 +758,14 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): self.entity_description.key, self._area_name, ) - self._attr_state = None + self._attr_native_value = None return if values := [x for x in data.values() if x is not None]: if self._mode == "avg": - self._attr_state = round(sum(values) / len(values), 1) + self._attr_native_value = round(sum(values) / len(values), 1) elif self._mode == "max": - self._attr_state = max(values) + self._attr_native_value = max(values) self._attr_available = self.state is not None self.async_write_ha_state() diff --git a/homeassistant/components/netdata/sensor.py b/homeassistant/components/netdata/sensor.py index 21e4cd1b005..d1fa87a6e5d 100644 --- a/homeassistant/components/netdata/sensor.py +++ b/homeassistant/components/netdata/sensor.py @@ -117,7 +117,7 @@ class NetdataSensor(SensorEntity): return f"{self._name} {self._sensor_name}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement @@ -127,7 +127,7 @@ class NetdataSensor(SensorEntity): return self._icon @property - def state(self): + def native_value(self): """Return the state of the resources.""" return self._state @@ -162,7 +162,7 @@ class NetdataAlarms(SensorEntity): return f"{self._name} Alarms" @property - def state(self): + def native_value(self): """Return the state of the resources.""" return self._state diff --git a/homeassistant/components/netgear_lte/sensor.py b/homeassistant/components/netgear_lte/sensor.py index c8f07301e98..0996ad3d315 100644 --- a/homeassistant/components/netgear_lte/sensor.py +++ b/homeassistant/components/netgear_lte/sensor.py @@ -37,7 +37,7 @@ class LTESensor(LTEEntity, SensorEntity): """Base LTE sensor entity.""" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return SENSOR_UNITS[self.sensor_type] @@ -46,7 +46,7 @@ class SMSUnreadSensor(LTESensor): """Unread SMS sensor entity.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return sum(1 for x in self.modem_data.data.sms if x.unread) @@ -55,7 +55,7 @@ class SMSTotalSensor(LTESensor): """Total SMS sensor entity.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return len(self.modem_data.data.sms) @@ -64,7 +64,7 @@ class UsageSensor(LTESensor): """Data usage sensor entity.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return round(self.modem_data.data.usage / 1024 ** 2, 1) @@ -73,6 +73,6 @@ class GenericSensor(LTESensor): """Sensor entity with raw state.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return getattr(self.modem_data.data, self.sensor_type) diff --git a/homeassistant/components/neurio_energy/sensor.py b/homeassistant/components/neurio_energy/sensor.py index d74d6338c8b..37113dde8b7 100644 --- a/homeassistant/components/neurio_energy/sensor.py +++ b/homeassistant/components/neurio_energy/sensor.py @@ -149,12 +149,12 @@ class NeurioEnergy(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/nexia/sensor.py b/homeassistant/components/nexia/sensor.py index a14931e41ee..6e44b8c9883 100644 --- a/homeassistant/components/nexia/sensor.py +++ b/homeassistant/components/nexia/sensor.py @@ -182,7 +182,7 @@ class NexiaThermostatSensor(NexiaThermostatEntity, SensorEntity): return self._class @property - def state(self): + def native_value(self): """Return the state of the sensor.""" val = getattr(self._thermostat, self._call)() if self._modifier: @@ -192,7 +192,7 @@ class NexiaThermostatSensor(NexiaThermostatEntity, SensorEntity): return val @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return self._unit_of_measurement @@ -230,7 +230,7 @@ class NexiaThermostatZoneSensor(NexiaThermostatZoneEntity, SensorEntity): return self._class @property - def state(self): + def native_value(self): """Return the state of the sensor.""" val = getattr(self._zone, self._call)() if self._modifier: @@ -240,6 +240,6 @@ class NexiaThermostatZoneSensor(NexiaThermostatZoneEntity, SensorEntity): return val @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return self._unit_of_measurement diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index fb03bcd25b5..f9df0d60412 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -146,7 +146,7 @@ class NextBusDepartureSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return current state of the sensor.""" return self._state diff --git a/homeassistant/components/nextcloud/sensor.py b/homeassistant/components/nextcloud/sensor.py index 5cd02f124e9..6a2d106bb10 100644 --- a/homeassistant/components/nextcloud/sensor.py +++ b/homeassistant/components/nextcloud/sensor.py @@ -34,7 +34,7 @@ class NextcloudSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state for this sensor.""" return self._state diff --git a/homeassistant/components/nightscout/sensor.py b/homeassistant/components/nightscout/sensor.py index 183755298d6..1b37fa8da7c 100644 --- a/homeassistant/components/nightscout/sensor.py +++ b/homeassistant/components/nightscout/sensor.py @@ -58,7 +58,7 @@ class NightscoutSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement @@ -68,7 +68,7 @@ class NightscoutSensor(SensorEntity): return self._available @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/nissan_leaf/sensor.py b/homeassistant/components/nissan_leaf/sensor.py index 936d607a84e..4074cd47f50 100644 --- a/homeassistant/components/nissan_leaf/sensor.py +++ b/homeassistant/components/nissan_leaf/sensor.py @@ -50,12 +50,12 @@ class LeafBatterySensor(LeafEntity, SensorEntity): return DEVICE_CLASS_BATTERY @property - def state(self): + def native_value(self): """Battery state percentage.""" return round(self.car.data[DATA_BATTERY]) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Battery state measured in percentage.""" return PERCENTAGE @@ -89,7 +89,7 @@ class LeafRangeSensor(LeafEntity, SensorEntity): ) @property - def state(self): + def native_value(self): """Battery range in miles or kms.""" if self._ac_on: ret = self.car.data[DATA_RANGE_AC] @@ -102,7 +102,7 @@ class LeafRangeSensor(LeafEntity, SensorEntity): return round(ret) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Battery range unit.""" if not self.car.hass.config.units.is_metric or self.car.force_miles: return LENGTH_MILES diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index 26f7dbd2c8a..72e51837bb8 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -120,7 +120,7 @@ class NMBSLiveBoard(SensorEntity): return DEFAULT_ICON @property - def state(self): + def native_value(self): """Return sensor state.""" return self._state @@ -166,7 +166,7 @@ class NMBSLiveBoard(SensorEntity): class NMBSSensor(SensorEntity): """Get the the total travel time for a given connection.""" - _attr_unit_of_measurement = TIME_MINUTES + _attr_native_unit_of_measurement = TIME_MINUTES def __init__( self, api_client, name, show_on_map, station_from, station_to, excl_vias @@ -238,7 +238,7 @@ class NMBSSensor(SensorEntity): return attrs @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/noaa_tides/sensor.py b/homeassistant/components/noaa_tides/sensor.py index e637e953173..5dbee551bb7 100644 --- a/homeassistant/components/noaa_tides/sensor.py +++ b/homeassistant/components/noaa_tides/sensor.py @@ -107,7 +107,7 @@ class NOAATidesAndCurrentsSensor(SensorEntity): return attr @property - def state(self): + def native_value(self): """Return the state of the device.""" if self.data is None: return None diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py index 48b9a25f783..cf6c394dbda 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -63,7 +63,7 @@ class NotionSensor(NotionEntity, SensorEntity): coordinator, task_id, sensor_id, bridge_id, system_id, name, device_class ) - self._attr_unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit @callback def _async_update_from_latest_data(self) -> None: @@ -71,7 +71,7 @@ class NotionSensor(NotionEntity, SensorEntity): task = self.coordinator.data["tasks"][self._task_id] if task["task_type"] == SENSOR_TEMPERATURE: - self._attr_state = round(float(task["status"]["value"]), 1) + self._attr_native_value = round(float(task["status"]["value"]), 1) else: LOGGER.error( "Unknown task type: %s: %s", diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py index 52536e69027..139728a3405 100644 --- a/homeassistant/components/nsw_fuel_station/sensor.py +++ b/homeassistant/components/nsw_fuel_station/sensor.py @@ -99,7 +99,7 @@ class StationPriceSensor(CoordinatorEntity, SensorEntity): return f"{station_name} {self._fuel_type}" @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the state of the sensor.""" if self.coordinator.data is None: return None @@ -117,7 +117,7 @@ class StationPriceSensor(CoordinatorEntity, SensorEntity): } @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the units of measurement.""" return f"{CURRENCY_CENT}/{VOLUME_LITERS}" diff --git a/homeassistant/components/numato/sensor.py b/homeassistant/components/numato/sensor.py index 19372de5258..fcf719c979e 100644 --- a/homeassistant/components/numato/sensor.py +++ b/homeassistant/components/numato/sensor.py @@ -78,12 +78,12 @@ class NumatoGpioAdc(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index b48121eeaf8..5bdd9049456 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -51,7 +51,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.status.display": SensorEntityDescription( key="ups.status.display", name="Status", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:information-outline", device_class=None, state_class=None, @@ -59,7 +59,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.status": SensorEntityDescription( key="ups.status", name="Status Data", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:information-outline", device_class=None, state_class=None, @@ -67,7 +67,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.alarm": SensorEntityDescription( key="ups.alarm", name="Alarms", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:alarm", device_class=None, state_class=None, @@ -75,7 +75,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.temperature": SensorEntityDescription( key="ups.temperature", name="UPS Temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, icon=None, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, @@ -83,7 +83,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.load": SensorEntityDescription( key="ups.load", name="Load", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", device_class=None, state_class=STATE_CLASS_MEASUREMENT, @@ -91,7 +91,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.load.high": SensorEntityDescription( key="ups.load.high", name="Overload Setting", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", device_class=None, state_class=None, @@ -99,7 +99,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.id": SensorEntityDescription( key="ups.id", name="System identifier", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:information-outline", device_class=None, state_class=None, @@ -107,7 +107,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.delay.start": SensorEntityDescription( key="ups.delay.start", name="Load Restart Delay", - unit_of_measurement=TIME_SECONDS, + native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", device_class=None, state_class=None, @@ -115,7 +115,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.delay.reboot": SensorEntityDescription( key="ups.delay.reboot", name="UPS Reboot Delay", - unit_of_measurement=TIME_SECONDS, + native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", device_class=None, state_class=None, @@ -123,7 +123,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.delay.shutdown": SensorEntityDescription( key="ups.delay.shutdown", name="UPS Shutdown Delay", - unit_of_measurement=TIME_SECONDS, + native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", device_class=None, state_class=None, @@ -131,7 +131,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.timer.start": SensorEntityDescription( key="ups.timer.start", name="Load Start Timer", - unit_of_measurement=TIME_SECONDS, + native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", device_class=None, state_class=None, @@ -139,7 +139,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.timer.reboot": SensorEntityDescription( key="ups.timer.reboot", name="Load Reboot Timer", - unit_of_measurement=TIME_SECONDS, + native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", device_class=None, state_class=None, @@ -147,7 +147,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.timer.shutdown": SensorEntityDescription( key="ups.timer.shutdown", name="Load Shutdown Timer", - unit_of_measurement=TIME_SECONDS, + native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", device_class=None, state_class=None, @@ -155,7 +155,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.test.interval": SensorEntityDescription( key="ups.test.interval", name="Self-Test Interval", - unit_of_measurement=TIME_SECONDS, + native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", device_class=None, state_class=None, @@ -163,7 +163,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.test.result": SensorEntityDescription( key="ups.test.result", name="Self-Test Result", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:information-outline", device_class=None, state_class=None, @@ -171,7 +171,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.test.date": SensorEntityDescription( key="ups.test.date", name="Self-Test Date", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:calendar", device_class=None, state_class=None, @@ -179,7 +179,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.display.language": SensorEntityDescription( key="ups.display.language", name="Language", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:information-outline", device_class=None, state_class=None, @@ -187,7 +187,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.contacts": SensorEntityDescription( key="ups.contacts", name="External Contacts", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:information-outline", device_class=None, state_class=None, @@ -195,7 +195,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.efficiency": SensorEntityDescription( key="ups.efficiency", name="Efficiency", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", device_class=None, state_class=STATE_CLASS_MEASUREMENT, @@ -203,7 +203,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.power": SensorEntityDescription( key="ups.power", name="Current Apparent Power", - unit_of_measurement=POWER_VOLT_AMPERE, + native_unit_of_measurement=POWER_VOLT_AMPERE, icon="mdi:flash", device_class=None, state_class=STATE_CLASS_MEASUREMENT, @@ -211,7 +211,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.power.nominal": SensorEntityDescription( key="ups.power.nominal", name="Nominal Power", - unit_of_measurement=POWER_VOLT_AMPERE, + native_unit_of_measurement=POWER_VOLT_AMPERE, icon="mdi:flash", device_class=None, state_class=None, @@ -219,7 +219,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.realpower": SensorEntityDescription( key="ups.realpower", name="Current Real Power", - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, icon=None, device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, @@ -227,7 +227,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.realpower.nominal": SensorEntityDescription( key="ups.realpower.nominal", name="Nominal Real Power", - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, icon=None, device_class=DEVICE_CLASS_POWER, state_class=None, @@ -235,7 +235,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.beeper.status": SensorEntityDescription( key="ups.beeper.status", name="Beeper Status", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:information-outline", device_class=None, state_class=None, @@ -243,7 +243,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.type": SensorEntityDescription( key="ups.type", name="UPS Type", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:information-outline", device_class=None, state_class=None, @@ -251,7 +251,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.watchdog.status": SensorEntityDescription( key="ups.watchdog.status", name="Watchdog Status", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:information-outline", device_class=None, state_class=None, @@ -259,7 +259,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.start.auto": SensorEntityDescription( key="ups.start.auto", name="Start on AC", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:information-outline", device_class=None, state_class=None, @@ -267,7 +267,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.start.battery": SensorEntityDescription( key="ups.start.battery", name="Start on Battery", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:information-outline", device_class=None, state_class=None, @@ -275,7 +275,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.start.reboot": SensorEntityDescription( key="ups.start.reboot", name="Reboot on Battery", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:information-outline", device_class=None, state_class=None, @@ -283,7 +283,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.shutdown": SensorEntityDescription( key="ups.shutdown", name="Shutdown Ability", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:information-outline", device_class=None, state_class=None, @@ -291,7 +291,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "battery.charge": SensorEntityDescription( key="battery.charge", name="Battery Charge", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon=None, device_class=DEVICE_CLASS_BATTERY, state_class=STATE_CLASS_MEASUREMENT, @@ -299,7 +299,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "battery.charge.low": SensorEntityDescription( key="battery.charge.low", name="Low Battery Setpoint", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", device_class=None, state_class=None, @@ -307,7 +307,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "battery.charge.restart": SensorEntityDescription( key="battery.charge.restart", name="Minimum Battery to Start", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", device_class=None, state_class=None, @@ -315,7 +315,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "battery.charge.warning": SensorEntityDescription( key="battery.charge.warning", name="Warning Battery Setpoint", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", device_class=None, state_class=None, @@ -323,7 +323,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "battery.charger.status": SensorEntityDescription( key="battery.charger.status", name="Charging Status", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:information-outline", device_class=None, state_class=None, @@ -331,7 +331,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "battery.voltage": SensorEntityDescription( key="battery.voltage", name="Battery Voltage", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon=None, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, @@ -339,7 +339,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "battery.voltage.nominal": SensorEntityDescription( key="battery.voltage.nominal", name="Nominal Battery Voltage", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon=None, device_class=DEVICE_CLASS_VOLTAGE, state_class=None, @@ -347,7 +347,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "battery.voltage.low": SensorEntityDescription( key="battery.voltage.low", name="Low Battery Voltage", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon=None, device_class=DEVICE_CLASS_VOLTAGE, state_class=None, @@ -355,7 +355,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "battery.voltage.high": SensorEntityDescription( key="battery.voltage.high", name="High Battery Voltage", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon=None, device_class=DEVICE_CLASS_VOLTAGE, state_class=None, @@ -363,7 +363,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "battery.capacity": SensorEntityDescription( key="battery.capacity", name="Battery Capacity", - unit_of_measurement="Ah", + native_unit_of_measurement="Ah", icon="mdi:flash", device_class=None, state_class=None, @@ -371,7 +371,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "battery.current": SensorEntityDescription( key="battery.current", name="Battery Current", - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, icon="mdi:flash", device_class=None, state_class=STATE_CLASS_MEASUREMENT, @@ -379,7 +379,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "battery.current.total": SensorEntityDescription( key="battery.current.total", name="Total Battery Current", - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, icon="mdi:flash", device_class=None, state_class=None, @@ -387,7 +387,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "battery.temperature": SensorEntityDescription( key="battery.temperature", name="Battery Temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, icon=None, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, @@ -395,7 +395,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "battery.runtime": SensorEntityDescription( key="battery.runtime", name="Battery Runtime", - unit_of_measurement=TIME_SECONDS, + native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", device_class=None, state_class=None, @@ -403,7 +403,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "battery.runtime.low": SensorEntityDescription( key="battery.runtime.low", name="Low Battery Runtime", - unit_of_measurement=TIME_SECONDS, + native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", device_class=None, state_class=None, @@ -411,7 +411,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "battery.runtime.restart": SensorEntityDescription( key="battery.runtime.restart", name="Minimum Battery Runtime to Start", - unit_of_measurement=TIME_SECONDS, + native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", device_class=None, state_class=None, @@ -419,7 +419,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "battery.alarm.threshold": SensorEntityDescription( key="battery.alarm.threshold", name="Battery Alarm Threshold", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:information-outline", device_class=None, state_class=None, @@ -427,7 +427,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "battery.date": SensorEntityDescription( key="battery.date", name="Battery Date", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:calendar", device_class=None, state_class=None, @@ -435,7 +435,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "battery.mfr.date": SensorEntityDescription( key="battery.mfr.date", name="Battery Manuf. Date", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:calendar", device_class=None, state_class=None, @@ -443,7 +443,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "battery.packs": SensorEntityDescription( key="battery.packs", name="Number of Batteries", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:information-outline", device_class=None, state_class=None, @@ -451,7 +451,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "battery.packs.bad": SensorEntityDescription( key="battery.packs.bad", name="Number of Bad Batteries", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:information-outline", device_class=None, state_class=None, @@ -459,7 +459,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "battery.type": SensorEntityDescription( key="battery.type", name="Battery Chemistry", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:information-outline", device_class=None, state_class=None, @@ -467,7 +467,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "input.sensitivity": SensorEntityDescription( key="input.sensitivity", name="Input Power Sensitivity", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:information-outline", device_class=None, state_class=None, @@ -475,7 +475,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "input.transfer.low": SensorEntityDescription( key="input.transfer.low", name="Low Voltage Transfer", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon=None, device_class=DEVICE_CLASS_VOLTAGE, state_class=None, @@ -483,7 +483,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "input.transfer.high": SensorEntityDescription( key="input.transfer.high", name="High Voltage Transfer", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon=None, device_class=DEVICE_CLASS_VOLTAGE, state_class=None, @@ -491,7 +491,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "input.transfer.reason": SensorEntityDescription( key="input.transfer.reason", name="Voltage Transfer Reason", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:information-outline", device_class=None, state_class=None, @@ -499,7 +499,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "input.voltage": SensorEntityDescription( key="input.voltage", name="Input Voltage", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon=None, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, @@ -507,7 +507,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "input.voltage.nominal": SensorEntityDescription( key="input.voltage.nominal", name="Nominal Input Voltage", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon=None, device_class=DEVICE_CLASS_VOLTAGE, state_class=None, @@ -515,7 +515,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "input.frequency": SensorEntityDescription( key="input.frequency", name="Input Line Frequency", - unit_of_measurement=FREQUENCY_HERTZ, + native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:flash", device_class=None, state_class=STATE_CLASS_MEASUREMENT, @@ -523,7 +523,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "input.frequency.nominal": SensorEntityDescription( key="input.frequency.nominal", name="Nominal Input Line Frequency", - unit_of_measurement=FREQUENCY_HERTZ, + native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:flash", device_class=None, state_class=None, @@ -531,7 +531,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "input.frequency.status": SensorEntityDescription( key="input.frequency.status", name="Input Frequency Status", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:information-outline", device_class=None, state_class=None, @@ -539,7 +539,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "output.current": SensorEntityDescription( key="output.current", name="Output Current", - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, icon="mdi:flash", device_class=None, state_class=STATE_CLASS_MEASUREMENT, @@ -547,7 +547,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "output.current.nominal": SensorEntityDescription( key="output.current.nominal", name="Nominal Output Current", - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, icon="mdi:flash", device_class=None, state_class=None, @@ -555,7 +555,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "output.voltage": SensorEntityDescription( key="output.voltage", name="Output Voltage", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon=None, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, @@ -563,7 +563,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "output.voltage.nominal": SensorEntityDescription( key="output.voltage.nominal", name="Nominal Output Voltage", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon=None, device_class=DEVICE_CLASS_VOLTAGE, state_class=None, @@ -571,7 +571,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "output.frequency": SensorEntityDescription( key="output.frequency", name="Output Frequency", - unit_of_measurement=FREQUENCY_HERTZ, + native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:flash", device_class=None, state_class=STATE_CLASS_MEASUREMENT, @@ -579,7 +579,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "output.frequency.nominal": SensorEntityDescription( key="output.frequency.nominal", name="Nominal Output Frequency", - unit_of_measurement=FREQUENCY_HERTZ, + native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:flash", device_class=None, state_class=None, @@ -587,7 +587,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ambient.humidity": SensorEntityDescription( key="ambient.humidity", name="Ambient Humidity", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon=None, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, @@ -595,7 +595,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ambient.temperature": SensorEntityDescription( key="ambient.temperature", name="Ambient Temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, icon=None, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 456778c3ca5..971c194c1c9 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -130,7 +130,7 @@ class NUTSensor(CoordinatorEntity, SensorEntity): return f"{self._unique_id}_{self.entity_description.key}" @property - def state(self): + def native_value(self): """Return entity state from ups.""" if not self._data.status: return None diff --git a/homeassistant/components/nws/const.py b/homeassistant/components/nws/const.py index 6e08ef408d3..32018bc40bb 100644 --- a/homeassistant/components/nws/const.py +++ b/homeassistant/components/nws/const.py @@ -113,7 +113,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Dew Point", icon=None, device_class=DEVICE_CLASS_TEMPERATURE, - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, unit_convert=TEMP_CELSIUS, ), NWSSensorEntityDescription( @@ -121,7 +121,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Temperature", icon=None, device_class=DEVICE_CLASS_TEMPERATURE, - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, unit_convert=TEMP_CELSIUS, ), NWSSensorEntityDescription( @@ -129,7 +129,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Wind Chill", icon=None, device_class=DEVICE_CLASS_TEMPERATURE, - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, unit_convert=TEMP_CELSIUS, ), NWSSensorEntityDescription( @@ -137,7 +137,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Heat Index", icon=None, device_class=DEVICE_CLASS_TEMPERATURE, - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, unit_convert=TEMP_CELSIUS, ), NWSSensorEntityDescription( @@ -145,7 +145,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Relative Humidity", icon=None, device_class=DEVICE_CLASS_HUMIDITY, - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, unit_convert=PERCENTAGE, ), NWSSensorEntityDescription( @@ -153,7 +153,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Wind Speed", icon="mdi:weather-windy", device_class=None, - unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, unit_convert=SPEED_MILES_PER_HOUR, ), NWSSensorEntityDescription( @@ -161,7 +161,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Wind Gust", icon="mdi:weather-windy", device_class=None, - unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, unit_convert=SPEED_MILES_PER_HOUR, ), NWSSensorEntityDescription( @@ -169,7 +169,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Wind Direction", icon="mdi:compass-rose", device_class=None, - unit_of_measurement=DEGREE, + native_unit_of_measurement=DEGREE, unit_convert=DEGREE, ), NWSSensorEntityDescription( @@ -177,7 +177,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Barometric Pressure", icon=None, device_class=DEVICE_CLASS_PRESSURE, - unit_of_measurement=PRESSURE_PA, + native_unit_of_measurement=PRESSURE_PA, unit_convert=PRESSURE_INHG, ), NWSSensorEntityDescription( @@ -185,7 +185,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Sea Level Pressure", icon=None, device_class=DEVICE_CLASS_PRESSURE, - unit_of_measurement=PRESSURE_PA, + native_unit_of_measurement=PRESSURE_PA, unit_convert=PRESSURE_INHG, ), NWSSensorEntityDescription( @@ -193,7 +193,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Visibility", icon="mdi:eye", device_class=None, - unit_of_measurement=LENGTH_METERS, + 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 409856831a2..85b60ffd475 100644 --- a/homeassistant/components/nws/sensor.py +++ b/homeassistant/components/nws/sensor.py @@ -73,16 +73,16 @@ class NWSSensor(CoordinatorEntity, SensorEntity): self._attr_name = f"{station} {description.name}" if not hass.config.units.is_metric: - self._attr_unit_of_measurement = description.unit_convert + self._attr_native_unit_of_measurement = description.unit_convert @property - def state(self): + def native_value(self): """Return the state.""" value = self._nws.observation.get(self.entity_description.key) if value is None: return None # Set alias to unit property -> prevent unnecessary hasattr calls - unit_of_measurement = self.unit_of_measurement + unit_of_measurement = self.native_unit_of_measurement if unit_of_measurement == SPEED_MILES_PER_HOUR: return round(convert_distance(value, LENGTH_KILOMETERS, LENGTH_MILES)) if unit_of_measurement == LENGTH_MILES: diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 97bced9e9c2..325438908a7 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -103,12 +103,12 @@ class NZBGetSensor(NZBGetEntity, SensorEntity): return self._unique_id @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit that the state of sensor is expressed in.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the state of the sensor.""" value = self.coordinator.data["status"].get(self._sensor_type) diff --git a/homeassistant/components/oasa_telematics/sensor.py b/homeassistant/components/oasa_telematics/sensor.py index 71af8dacba2..4c9b583a36b 100644 --- a/homeassistant/components/oasa_telematics/sensor.py +++ b/homeassistant/components/oasa_telematics/sensor.py @@ -73,7 +73,7 @@ class OASATelematicsSensor(SensorEntity): return DEVICE_CLASS_TIMESTAMP @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py index 639b9eb332f..7ba28ee0741 100644 --- a/homeassistant/components/obihai/sensor.py +++ b/homeassistant/components/obihai/sensor.py @@ -85,7 +85,7 @@ class ObihaiServiceSensors(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index 16f6efce004..5b2b0af494c 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -107,7 +107,7 @@ class OctoPrintSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" sensor_unit = self.unit_of_measurement if sensor_unit in (TEMP_CELSIUS, PERCENTAGE): @@ -118,7 +118,7 @@ class OctoPrintSensor(SensorEntity): return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/ohmconnect/sensor.py b/homeassistant/components/ohmconnect/sensor.py index b53c35e17b5..0638b32d105 100644 --- a/homeassistant/components/ohmconnect/sensor.py +++ b/homeassistant/components/ohmconnect/sensor.py @@ -48,7 +48,7 @@ class OhmconnectSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._data.get("active") == "True": return "Active" diff --git a/homeassistant/components/ombi/sensor.py b/homeassistant/components/ombi/sensor.py index c91cf429c94..50bb121dc4b 100644 --- a/homeassistant/components/ombi/sensor.py +++ b/homeassistant/components/ombi/sensor.py @@ -53,7 +53,7 @@ class OmbiSensor(SensorEntity): return self._icon @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/omnilogic/sensor.py b/homeassistant/components/omnilogic/sensor.py index 1f8de082868..f0382c01342 100644 --- a/homeassistant/components/omnilogic/sensor.py +++ b/homeassistant/components/omnilogic/sensor.py @@ -86,7 +86,7 @@ class OmnilogicSensor(OmniLogicEntity, SensorEntity): return self._device_class @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the right unit of measure.""" return self._unit @@ -95,7 +95,7 @@ class OmniLogicTemperatureSensor(OmnilogicSensor): """Define an OmniLogic Temperature (Air/Water) Sensor.""" @property - def state(self): + def native_value(self): """Return the state for the temperature sensor.""" sensor_data = self.coordinator.data[self._item_id][self._state_key] @@ -123,7 +123,7 @@ class OmniLogicPumpSpeedSensor(OmnilogicSensor): """Define an OmniLogic Pump Speed Sensor.""" @property - def state(self): + def native_value(self): """Return the state for the pump speed sensor.""" pump_type = PUMP_TYPES[ @@ -158,7 +158,7 @@ class OmniLogicSaltLevelSensor(OmnilogicSensor): """Define an OmniLogic Salt Level Sensor.""" @property - def state(self): + def native_value(self): """Return the state for the salt level sensor.""" salt_return = self.coordinator.data[self._item_id][self._state_key] @@ -177,7 +177,7 @@ class OmniLogicChlorinatorSensor(OmnilogicSensor): """Define an OmniLogic Chlorinator Sensor.""" @property - def state(self): + def native_value(self): """Return the state for the chlorinator sensor.""" state = self.coordinator.data[self._item_id][self._state_key] @@ -188,7 +188,7 @@ class OmniLogicPHSensor(OmnilogicSensor): """Define an OmniLogic pH Sensor.""" @property - def state(self): + def native_value(self): """Return the state for the pH sensor.""" ph_state = self.coordinator.data[self._item_id][self._state_key] @@ -232,7 +232,7 @@ class OmniLogicORPSensor(OmnilogicSensor): ) @property - def state(self): + def native_value(self): """Return the state for the ORP sensor.""" orp_state = int(self.coordinator.data[self._item_id][self._state_key]) diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index 7449524d9e5..693d685f77c 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -28,49 +28,49 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="temperature", name="Temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, icon=None, device_class=DEVICE_CLASS_TEMPERATURE, ), SensorEntityDescription( key="orp", name="Oxydo Reduction Potential", - unit_of_measurement=ELECTRIC_POTENTIAL_MILLIVOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_MILLIVOLT, icon="mdi:pool", device_class=None, ), SensorEntityDescription( key="ph", name="pH", - unit_of_measurement=None, + native_unit_of_measurement=None, icon="mdi:pool", device_class=None, ), SensorEntityDescription( key="tds", name="TDS", - unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, icon="mdi:pool", device_class=None, ), SensorEntityDescription( key="battery", name="Battery", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon=None, device_class=DEVICE_CLASS_BATTERY, ), SensorEntityDescription( key="rssi", name="RSSI", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon=None, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, ), SensorEntityDescription( key="salt", name="Salt", - unit_of_measurement="mg/L", + native_unit_of_measurement="mg/L", icon="mdi:pool", device_class=None, ), @@ -164,7 +164,7 @@ class OndiloICO(CoordinatorEntity, SensorEntity): ) @property - def state(self): + def native_value(self): """Last value of the sensor.""" return self._devdata()["value"] diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 024d540c10a..215ba6c569b 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -368,7 +368,7 @@ class OneWireSensor(OneWireBaseEntity, SensorEntity): """Mixin for sensor specific attributes.""" @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" return self._unit_of_measurement @@ -377,7 +377,7 @@ class OneWireProxySensor(OneWireProxyEntity, OneWireSensor): """Implementation of a 1-Wire sensor connected through owserver.""" @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the entity.""" return self._state @@ -405,7 +405,7 @@ class OneWireDirectSensor(OneWireSensor): self._owsensor = owsensor @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the entity.""" return self._state diff --git a/homeassistant/components/onvif/sensor.py b/homeassistant/components/onvif/sensor.py index 1c5766e3969..5c31644ba19 100644 --- a/homeassistant/components/onvif/sensor.py +++ b/homeassistant/components/onvif/sensor.py @@ -44,7 +44,7 @@ class ONVIFSensor(ONVIFBaseEntity, SensorEntity): super().__init__(device) @property - def state(self) -> None | str | int | float: + def native_value(self) -> None | str | int | float: """Return the state of the entity.""" return self.device.events.get_uid(self.uid).value @@ -59,7 +59,7 @@ class ONVIFSensor(ONVIFBaseEntity, SensorEntity): return self.device.events.get_uid(self.uid).device_class @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" return self.device.events.get_uid(self.uid).unit_of_measurement diff --git a/homeassistant/components/openerz/sensor.py b/homeassistant/components/openerz/sensor.py index 33305b677de..8a3c2c0460d 100644 --- a/homeassistant/components/openerz/sensor.py +++ b/homeassistant/components/openerz/sensor.py @@ -44,7 +44,7 @@ class OpenERZSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/openevse/sensor.py b/homeassistant/components/openevse/sensor.py index 29eeceb232c..a1920e145bb 100644 --- a/homeassistant/components/openevse/sensor.py +++ b/homeassistant/components/openevse/sensor.py @@ -70,12 +70,12 @@ class OpenEVSESensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this sensor.""" return self._unit_of_measurement diff --git a/homeassistant/components/openexchangerates/sensor.py b/homeassistant/components/openexchangerates/sensor.py index 8474cdab131..803123a88c3 100644 --- a/homeassistant/components/openexchangerates/sensor.py +++ b/homeassistant/components/openexchangerates/sensor.py @@ -73,7 +73,7 @@ class OpenexchangeratesSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/openhardwaremonitor/sensor.py b/homeassistant/components/openhardwaremonitor/sensor.py index 8f43c1e5e9b..280acab5d0e 100644 --- a/homeassistant/components/openhardwaremonitor/sensor.py +++ b/homeassistant/components/openhardwaremonitor/sensor.py @@ -62,12 +62,12 @@ class OpenHardwareMonitorDevice(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the state of the device.""" return self.value diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py index 122388b85b7..0502d6c6573 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -107,7 +107,7 @@ class OpenSkySensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -178,7 +178,7 @@ class OpenSkySensor(SensorEntity): return {ATTR_ATTRIBUTION: OPENSKY_ATTRIBUTION} @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return "flights" diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index 1d9904ea59f..28f139f188f 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -156,12 +156,12 @@ class OpenThermSensor(SensorEntity): return self._device_class @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._value @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index 386527ebc3e..e115f9294a5 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -105,7 +105,7 @@ class OpenUvSensor(OpenUvEntity, SensorEntity): self._attr_icon = icon self._attr_name = name - self._attr_unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit @callback def update_from_latest_data(self) -> None: @@ -119,22 +119,22 @@ class OpenUvSensor(OpenUvEntity, SensorEntity): self._attr_available = True if self._sensor_type == TYPE_CURRENT_OZONE_LEVEL: - self._attr_state = data["ozone"] + self._attr_native_value = data["ozone"] elif self._sensor_type == TYPE_CURRENT_UV_INDEX: - self._attr_state = data["uv"] + self._attr_native_value = data["uv"] elif self._sensor_type == TYPE_CURRENT_UV_LEVEL: if data["uv"] >= 11: - self._attr_state = UV_LEVEL_EXTREME + self._attr_native_value = UV_LEVEL_EXTREME elif data["uv"] >= 8: - self._attr_state = UV_LEVEL_VHIGH + self._attr_native_value = UV_LEVEL_VHIGH elif data["uv"] >= 6: - self._attr_state = UV_LEVEL_HIGH + self._attr_native_value = UV_LEVEL_HIGH elif data["uv"] >= 3: - self._attr_state = UV_LEVEL_MODERATE + self._attr_native_value = UV_LEVEL_MODERATE else: - self._attr_state = UV_LEVEL_LOW + self._attr_native_value = UV_LEVEL_LOW elif self._sensor_type == TYPE_MAX_UV_INDEX: - self._attr_state = data["uv_max"] + self._attr_native_value = data["uv_max"] uv_max_time = parse_datetime(data["uv_max_time"]) if uv_max_time: self._attr_extra_state_attributes.update( @@ -148,6 +148,6 @@ class OpenUvSensor(OpenUvEntity, SensorEntity): TYPE_SAFE_EXPOSURE_TIME_5, TYPE_SAFE_EXPOSURE_TIME_6, ): - self._attr_state = data["safe_exposure_time"][ + self._attr_native_value = data["safe_exposure_time"][ EXPOSURE_TYPE_MAP[self._sensor_type] ] diff --git a/homeassistant/components/openweathermap/abstract_owm_sensor.py b/homeassistant/components/openweathermap/abstract_owm_sensor.py index ea12123b707..3c66ca50f3c 100644 --- a/homeassistant/components/openweathermap/abstract_owm_sensor.py +++ b/homeassistant/components/openweathermap/abstract_owm_sensor.py @@ -71,7 +71,7 @@ class AbstractOpenWeatherMapSensor(SensorEntity): return self._device_class @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 39c50c3b941..3586f958a6a 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -68,7 +68,7 @@ class OpenWeatherMapSensor(AbstractOpenWeatherMapSensor): self._weather_coordinator = weather_coordinator @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._weather_coordinator.data.get(self._sensor_type, None) @@ -91,7 +91,7 @@ class OpenWeatherMapForecastSensor(AbstractOpenWeatherMapSensor): self._weather_coordinator = weather_coordinator @property - def state(self): + def native_value(self): """Return the state of the device.""" forecasts = self._weather_coordinator.data.get(ATTR_API_FORECAST) if forecasts is not None and len(forecasts) > 0: diff --git a/homeassistant/components/oru/sensor.py b/homeassistant/components/oru/sensor.py index c17873aefea..dc2b7216534 100644 --- a/homeassistant/components/oru/sensor.py +++ b/homeassistant/components/oru/sensor.py @@ -42,7 +42,7 @@ class CurrentEnergyUsageSensor(SensorEntity): """Representation of the sensor.""" _attr_icon = SENSOR_ICON - _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR + _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR def __init__(self, meter): """Initialize the sensor.""" @@ -61,7 +61,7 @@ class CurrentEnergyUsageSensor(SensorEntity): return SENSOR_NAME @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/otp/sensor.py b/homeassistant/components/otp/sensor.py index 7aee9d99208..45bedb6b499 100644 --- a/homeassistant/components/otp/sensor.py +++ b/homeassistant/components/otp/sensor.py @@ -63,7 +63,7 @@ class TOTPSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index f678caf02b0..91290238dce 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -81,7 +81,7 @@ class OVOEnergySensor(OVOEnergyDeviceEntity, SensorEntity): super().__init__(coordinator, client, key, name, icon) @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit this state is expressed in.""" return self._unit_of_measurement @@ -103,7 +103,7 @@ class OVOEnergyLastElectricityReading(OVOEnergySensor): ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" usage: OVODailyUsage = self.coordinator.data if usage is None or not usage.electricity: @@ -139,7 +139,7 @@ class OVOEnergyLastGasReading(OVOEnergySensor): ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" usage: OVODailyUsage = self.coordinator.data if usage is None or not usage.gas: @@ -176,7 +176,7 @@ class OVOEnergyLastElectricityCost(OVOEnergySensor): ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" usage: OVODailyUsage = self.coordinator.data if usage is None or not usage.electricity: @@ -213,7 +213,7 @@ class OVOEnergyLastGasCost(OVOEnergySensor): ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" usage: OVODailyUsage = self.coordinator.data if usage is None or not usage.gas: diff --git a/homeassistant/components/ozw/sensor.py b/homeassistant/components/ozw/sensor.py index 0ff08a87d16..97b7b01d4d4 100644 --- a/homeassistant/components/ozw/sensor.py +++ b/homeassistant/components/ozw/sensor.py @@ -106,12 +106,12 @@ class ZWaveStringSensor(ZwaveSensorBase): """Representation of a Z-Wave sensor.""" @property - def state(self): + def native_value(self): """Return state of the sensor.""" return self.values.primary.value @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return unit of measurement the value is expressed in.""" return self.values.primary.units @@ -125,12 +125,12 @@ class ZWaveNumericSensor(ZwaveSensorBase): """Representation of a Z-Wave sensor.""" @property - def state(self): + def native_value(self): """Return state of the sensor.""" return round(self.values.primary.value, 2) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return unit of measurement the value is expressed in.""" if self.values.primary.units == "C": return TEMP_CELSIUS @@ -144,7 +144,7 @@ class ZWaveListSensor(ZwaveSensorBase): """Representation of a Z-Wave list sensor.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" # We use the id as value for backwards compatibility return self.values.primary.value["Selected_id"] diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index 52c638864a5..f1ec1c6efd6 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -41,55 +41,55 @@ SENSOR_TYPES: tuple[PiHoleSensorEntityDescription, ...] = ( PiHoleSensorEntityDescription( key="ads_blocked_today", name="Ads Blocked Today", - unit_of_measurement="ads", + native_unit_of_measurement="ads", icon="mdi:close-octagon-outline", ), PiHoleSensorEntityDescription( key="ads_percentage_today", name="Ads Percentage Blocked Today", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:close-octagon-outline", ), PiHoleSensorEntityDescription( key="clients_ever_seen", name="Seen Clients", - unit_of_measurement="clients", + native_unit_of_measurement="clients", icon="mdi:account-outline", ), PiHoleSensorEntityDescription( key="dns_queries_today", name="DNS Queries Today", - unit_of_measurement="queries", + native_unit_of_measurement="queries", icon="mdi:comment-question-outline", ), PiHoleSensorEntityDescription( key="domains_being_blocked", name="Domains Blocked", - unit_of_measurement="domains", + native_unit_of_measurement="domains", icon="mdi:block-helper", ), PiHoleSensorEntityDescription( key="queries_cached", name="DNS Queries Cached", - unit_of_measurement="queries", + native_unit_of_measurement="queries", icon="mdi:comment-question-outline", ), PiHoleSensorEntityDescription( key="queries_forwarded", name="DNS Queries Forwarded", - unit_of_measurement="queries", + native_unit_of_measurement="queries", icon="mdi:comment-question-outline", ), PiHoleSensorEntityDescription( key="unique_clients", name="DNS Unique Clients", - unit_of_measurement="clients", + native_unit_of_measurement="clients", icon="mdi:account-outline", ), PiHoleSensorEntityDescription( key="unique_domains", name="DNS Unique Domains", - unit_of_measurement="domains", + native_unit_of_measurement="domains", icon="mdi:domain", ), ) diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index 14aed86a479..0e231868647 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -63,7 +63,7 @@ class PiHoleSensor(PiHoleEntity, SensorEntity): self._attr_unique_id = f"{self._server_unique_id}/{description.name}" @property - def state(self) -> Any: + def native_value(self) -> Any: """Return the state of the device.""" try: return round(self.api.data[self.entity_description.key], 2) diff --git a/homeassistant/components/picnic/sensor.py b/homeassistant/components/picnic/sensor.py index 3a4d3582f9c..57f24180c03 100644 --- a/homeassistant/components/picnic/sensor.py +++ b/homeassistant/components/picnic/sensor.py @@ -3,6 +3,7 @@ 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 @@ -30,7 +31,7 @@ async def async_setup_entry( return True -class PicnicSensor(CoordinatorEntity): +class PicnicSensor(SensorEntity, CoordinatorEntity): """The CoordinatorEntity subclass representing Picnic sensors.""" def __init__( @@ -49,7 +50,7 @@ class PicnicSensor(CoordinatorEntity): self._service_unique_id = config_entry.unique_id @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit this state is expressed in.""" return self.properties.get("unit") @@ -64,7 +65,7 @@ class PicnicSensor(CoordinatorEntity): return self._to_capitalized_name(self.sensor_type) @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the entity.""" data_set = ( self.coordinator.data.get(self.properties["data_type"], {}) diff --git a/homeassistant/components/pilight/sensor.py b/homeassistant/components/pilight/sensor.py index 97458acd5fc..bc8135c5932 100644 --- a/homeassistant/components/pilight/sensor.py +++ b/homeassistant/components/pilight/sensor.py @@ -63,12 +63,12 @@ class PilightSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the state of the entity.""" return self._state diff --git a/homeassistant/components/plaato/sensor.py b/homeassistant/components/plaato/sensor.py index 9af16a1cacd..e3e37d4291e 100644 --- a/homeassistant/components/plaato/sensor.py +++ b/homeassistant/components/plaato/sensor.py @@ -75,11 +75,11 @@ class PlaatoSensor(PlaatoEntity, SensorEntity): return None @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._sensor_data.sensors.get(self._sensor_type) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._sensor_data.get_unit_of_measurement(self._sensor_type) diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 8ca72e8fb83..0969967e673 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -62,7 +62,7 @@ class PlexSensor(SensorEntity): self._attr_name = NAME_FORMAT.format(plex_server.friendly_name) self._attr_should_poll = False self._attr_unique_id = f"sensor-{plex_server.machine_identifier}" - self._attr_unit_of_measurement = "Watching" + self._attr_native_unit_of_measurement = "Watching" self._server = plex_server self.async_refresh_sensor = Debouncer( @@ -87,7 +87,7 @@ class PlexSensor(SensorEntity): async def _async_refresh_sensor(self): """Set instance object and trigger an entity state update.""" _LOGGER.debug("Refreshing sensor [%s]", self.unique_id) - self._attr_state = len(self._server.sensor_attributes) + self._attr_native_value = len(self._server.sensor_attributes) self.async_write_ha_state() @property @@ -128,7 +128,7 @@ class PlexLibrarySectionSensor(SensorEntity): self._attr_name = f"{self.server_name} Library - {plex_library_section.title}" self._attr_should_poll = False self._attr_unique_id = f"library-{self.server_id}-{plex_library_section.uuid}" - self._attr_unit_of_measurement = "Items" + self._attr_native_unit_of_measurement = "Items" async def async_added_to_hass(self): """Run when about to be added to hass.""" @@ -164,7 +164,7 @@ class PlexLibrarySectionSensor(SensorEntity): self.library_type, self.library_type ) - self._attr_state = self.library_section.totalViewSize( + self._attr_native_value = self.library_section.totalViewSize( libtype=primary_libtype, includeCollections=False ) for libtype in LIBRARY_ATTRIBUTE_TYPES.get(self.library_type, []): diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 4152f9fdabd..854c2e6676c 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -272,12 +272,12 @@ class SmileSensor(SmileGateway, SensorEntity): return self._icon @property - def state(self): + def native_value(self): """Return the state of this entity.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/pocketcasts/sensor.py b/homeassistant/components/pocketcasts/sensor.py index 55ae4a524fc..f745bd562bd 100644 --- a/homeassistant/components/pocketcasts/sensor.py +++ b/homeassistant/components/pocketcasts/sensor.py @@ -50,7 +50,7 @@ class PocketCastsSensor(SensorEntity): return SENSOR_NAME @property - def state(self): + def native_value(self): """Return the sensor state.""" return self._state diff --git a/homeassistant/components/point/sensor.py b/homeassistant/components/point/sensor.py index dc34e1f9367..87981d7b29e 100644 --- a/homeassistant/components/point/sensor.py +++ b/homeassistant/components/point/sensor.py @@ -70,13 +70,13 @@ class MinutPointSensor(MinutPointEntity, SensorEntity): return self._device_prop[0] @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.value is None: return None return round(self.value, self._device_prop[1]) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._device_prop[2] diff --git a/homeassistant/components/poolsense/sensor.py b/homeassistant/components/poolsense/sensor.py index dd03111e85e..e9aeaca20f5 100644 --- a/homeassistant/components/poolsense/sensor.py +++ b/homeassistant/components/poolsense/sensor.py @@ -89,7 +89,7 @@ class PoolSenseSensor(PoolSenseEntity, SensorEntity): return f"PoolSense {SENSORS[self.info_type]['name']}" @property - def state(self): + def native_value(self): """State of the sensor.""" return self.coordinator.data[self.info_type] @@ -104,7 +104,7 @@ class PoolSenseSensor(PoolSenseEntity, SensorEntity): return SENSORS[self.info_type]["icon"] @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return unit of measurement.""" return SENSORS[self.info_type]["unit"] diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index b2281c515ae..96542dc3929 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -87,7 +87,7 @@ class PowerWallChargeSensor(PowerWallEntity, SensorEntity): """Representation of an Powerwall charge sensor.""" _attr_name = "Powerwall Charge" - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE _attr_device_class = DEVICE_CLASS_BATTERY @property @@ -96,7 +96,7 @@ class PowerWallChargeSensor(PowerWallEntity, SensorEntity): return f"{self.base_unique_id}_charge" @property - def state(self): + def native_value(self): """Get the current value in percentage.""" return round(self.coordinator.data[POWERWALL_API_CHARGE]) @@ -105,7 +105,7 @@ class PowerWallEnergySensor(PowerWallEntity, SensorEntity): """Representation of an Powerwall Energy sensor.""" _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_unit_of_measurement = POWER_KILO_WATT + _attr_native_unit_of_measurement = POWER_KILO_WATT _attr_device_class = DEVICE_CLASS_POWER def __init__( @@ -128,7 +128,7 @@ class PowerWallEnergySensor(PowerWallEntity, SensorEntity): ) @property - def state(self): + def native_value(self): """Get the current value in kW.""" return ( self.coordinator.data[POWERWALL_API_METERS] diff --git a/homeassistant/components/pushbullet/sensor.py b/homeassistant/components/pushbullet/sensor.py index 4f8ec6a1700..3585c198a51 100644 --- a/homeassistant/components/pushbullet/sensor.py +++ b/homeassistant/components/pushbullet/sensor.py @@ -79,7 +79,7 @@ class PushBulletNotificationSensor(SensorEntity): return f"Pushbullet {self._element}" @property - def state(self): + def native_value(self): """Return the current state of the sensor.""" return self._state diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 305615e4b2c..722aea8e868 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -79,7 +79,7 @@ class PvoutputSensor(SensorEntity, RestoreEntity): _attr_state_class = STATE_CLASS_MEASUREMENT _attr_device_class = DEVICE_CLASS_ENERGY - _attr_unit_of_measurement = ENERGY_WATT_HOUR + _attr_native_unit_of_measurement = ENERGY_WATT_HOUR _old_state: int | None = None @@ -104,7 +104,7 @@ class PvoutputSensor(SensorEntity, RestoreEntity): ) @property - def state(self): + def native_value(self): """Return the state of the device.""" if self.pvcoutput is not None: return self.pvcoutput.energy_generation diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py index 75881f93f0a..157327fbd19 100644 --- a/homeassistant/components/pvpc_hourly_pricing/sensor.py +++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py @@ -106,7 +106,7 @@ class ElecPriceSensor(RestoreEntity, SensorEntity): return self._name @property - def state(self) -> float: + def native_value(self) -> float: """Return the state of the sensor.""" return self._pvpc_data.state diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index c439d5181be..f568b41776f 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -93,12 +93,12 @@ class PyLoadSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index 251407099b1..5f57cd19cfe 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -90,7 +90,7 @@ class QBittorrentSensor(SensorEntity): return f"{self.client_name} {self._name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -100,7 +100,7 @@ class QBittorrentSensor(SensorEntity): return self._available @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index c175d89f60e..333ce46599a 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -243,7 +243,7 @@ class QNAPSensor(SensorEntity): return self.var_icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self.var_units @@ -256,7 +256,7 @@ class QNAPCPUSensor(QNAPSensor): """A QNAP sensor that monitors CPU stats.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.var_id == "cpu_temp": return self._api.data["system_stats"]["cpu"]["temp_c"] @@ -268,7 +268,7 @@ class QNAPMemorySensor(QNAPSensor): """A QNAP sensor that monitors memory stats.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" free = float(self._api.data["system_stats"]["memory"]["free"]) / 1024 if self.var_id == "memory_free": @@ -296,7 +296,7 @@ class QNAPNetworkSensor(QNAPSensor): """A QNAP sensor that monitors network stats.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.var_id == "network_link_status": nic = self._api.data["system_stats"]["nics"][self.monitor_device] @@ -329,7 +329,7 @@ class QNAPSystemSensor(QNAPSensor): """A QNAP sensor that monitors overall system health.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.var_id == "status": return self._api.data["system_health"] @@ -358,7 +358,7 @@ class QNAPDriveSensor(QNAPSensor): """A QNAP sensor that monitors HDD/SSD drive stats.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" data = self._api.data["smart_drive_health"][self.monitor_device] @@ -392,7 +392,7 @@ class QNAPVolumeSensor(QNAPSensor): """A QNAP sensor that monitors storage volume stats.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" data = self._api.data["volumes"][self.monitor_device] diff --git a/homeassistant/components/qwikswitch/sensor.py b/homeassistant/components/qwikswitch/sensor.py index f6d0ce7ec28..5de7bc0dccf 100644 --- a/homeassistant/components/qwikswitch/sensor.py +++ b/homeassistant/components/qwikswitch/sensor.py @@ -57,7 +57,7 @@ class QSSensor(QSEntity, SensorEntity): self.async_write_ha_state() @property - def state(self): + def native_value(self): """Return the value of the sensor.""" return str(self._val) @@ -67,6 +67,6 @@ class QSSensor(QSEntity, SensorEntity): return f"qs{self.qsid}:{self.channel}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self.unit From 6de6a5dc141c433a1f4761ccb052bbd37115f746 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 12 Aug 2021 14:23:56 +0200 Subject: [PATCH 174/355] Move temperature conversions to sensor base class (3/8) (#54469) * Move temperature conversions to entity base class (3/8) * Fix FritzBox sensor * Fix tests --- homeassistant/components/fail2ban/sensor.py | 2 +- homeassistant/components/fastdotcom/sensor.py | 8 +- homeassistant/components/fibaro/sensor.py | 4 +- homeassistant/components/fido/sensor.py | 4 +- homeassistant/components/file/sensor.py | 4 +- homeassistant/components/filesize/sensor.py | 4 +- homeassistant/components/filter/sensor.py | 4 +- homeassistant/components/fints/sensor.py | 8 +- .../components/fireservicerota/sensor.py | 2 +- homeassistant/components/firmata/sensor.py | 2 +- homeassistant/components/fitbit/sensor.py | 4 +- homeassistant/components/fixer/sensor.py | 4 +- .../components/flick_electric/sensor.py | 4 +- homeassistant/components/flo/sensor.py | 26 +-- homeassistant/components/flume/sensor.py | 4 +- homeassistant/components/flunearyou/sensor.py | 8 +- homeassistant/components/folder/sensor.py | 4 +- homeassistant/components/foobot/sensor.py | 4 +- .../components/forecast_solar/const.py | 16 +- .../components/forecast_solar/sensor.py | 2 +- homeassistant/components/freebox/sensor.py | 4 +- homeassistant/components/freedompro/sensor.py | 6 +- homeassistant/components/fritz/sensor.py | 6 +- homeassistant/components/fritzbox/__init__.py | 7 - homeassistant/components/fritzbox/sensor.py | 31 +++- .../components/fritzbox_callmonitor/sensor.py | 2 +- homeassistant/components/fronius/sensor.py | 4 +- .../components/garages_amsterdam/sensor.py | 4 +- homeassistant/components/gdacs/sensor.py | 4 +- homeassistant/components/geniushub/sensor.py | 6 +- .../components/geo_rss_events/sensor.py | 4 +- .../components/geonetnz_quakes/sensor.py | 4 +- .../components/geonetnz_volcano/sensor.py | 4 +- homeassistant/components/gios/const.py | 14 +- homeassistant/components/gios/sensor.py | 4 +- homeassistant/components/github/sensor.py | 2 +- homeassistant/components/gitlab_ci/sensor.py | 2 +- homeassistant/components/gitter/sensor.py | 4 +- homeassistant/components/glances/const.py | 44 ++--- homeassistant/components/glances/sensor.py | 2 +- homeassistant/components/goalzero/sensor.py | 4 +- homeassistant/components/gogogate2/sensor.py | 6 +- .../components/google_travel_time/sensor.py | 4 +- .../components/google_wifi/sensor.py | 4 +- homeassistant/components/gpsd/sensor.py | 2 +- .../components/greeneye_monitor/sensor.py | 16 +- .../components/growatt_server/sensor.py | 168 +++++++++--------- homeassistant/components/gtfs/sensor.py | 2 +- homeassistant/components/guardian/sensor.py | 18 +- homeassistant/components/habitica/sensor.py | 8 +- homeassistant/components/hassio/sensor.py | 4 +- .../components/haveibeenpwned/sensor.py | 4 +- homeassistant/components/hddtemp/sensor.py | 4 +- .../components/here_travel_time/sensor.py | 4 +- .../components/history_stats/sensor.py | 4 +- homeassistant/components/hive/sensor.py | 4 +- .../components/home_connect/sensor.py | 4 +- .../components/homekit_controller/sensor.py | 24 +-- homeassistant/components/homematic/sensor.py | 4 +- .../components/homematicip_cloud/sensor.py | 34 ++-- homeassistant/components/hp_ilo/sensor.py | 4 +- homeassistant/components/htu21d/sensor.py | 4 +- homeassistant/components/huawei_lte/sensor.py | 4 +- homeassistant/components/hue/sensor.py | 12 +- homeassistant/components/huisbaasje/sensor.py | 4 +- .../hunterdouglas_powerview/sensor.py | 4 +- .../components/hvv_departures/sensor.py | 2 +- homeassistant/components/hydrawise/sensor.py | 4 +- tests/components/fail2ban/test_sensor.py | 8 + tests/components/google_wifi/test_sensor.py | 30 ++-- 70 files changed, 350 insertions(+), 324 deletions(-) diff --git a/homeassistant/components/fail2ban/sensor.py b/homeassistant/components/fail2ban/sensor.py index 908ab5d77c0..5a7e1052b67 100644 --- a/homeassistant/components/fail2ban/sensor.py +++ b/homeassistant/components/fail2ban/sensor.py @@ -70,7 +70,7 @@ class BanSensor(SensorEntity): return self.ban_dict @property - def state(self): + def native_value(self): """Return the most recently banned IP Address.""" return self.last_ban diff --git a/homeassistant/components/fastdotcom/sensor.py b/homeassistant/components/fastdotcom/sensor.py index 14f63a99e5d..fa1f18815f1 100644 --- a/homeassistant/components/fastdotcom/sensor.py +++ b/homeassistant/components/fastdotcom/sensor.py @@ -30,10 +30,10 @@ class SpeedtestSensor(RestoreEntity, SensorEntity): """Implementation of a FAst.com sensor.""" _attr_name = "Fast.com Download" - _attr_unit_of_measurement = DATA_RATE_MEGABITS_PER_SECOND + _attr_native_unit_of_measurement = DATA_RATE_MEGABITS_PER_SECOND _attr_icon = ICON _attr_should_poll = False - _attr_state = None + _attr_native_value = None def __init__(self, speedtest_data: dict[str, Any]) -> None: """Initialize the sensor.""" @@ -52,14 +52,14 @@ class SpeedtestSensor(RestoreEntity, SensorEntity): state = await self.async_get_last_state() if not state: return - self._attr_state = state.state + self._attr_native_value = state.state def update(self) -> None: """Get the latest data and update the states.""" data = self._speedtest_data.data # type: ignore[attr-defined] if data is None: return - self._attr_state = data["download"] + self._attr_native_value = data["download"] @callback def _schedule_immediate_update(self) -> None: diff --git a/homeassistant/components/fibaro/sensor.py b/homeassistant/components/fibaro/sensor.py index 3161e173b2a..a4b4e744af7 100644 --- a/homeassistant/components/fibaro/sensor.py +++ b/homeassistant/components/fibaro/sensor.py @@ -85,12 +85,12 @@ class FibaroSensor(FibaroDevice, SensorEntity): self._unit = self.fibaro_device.properties.unit @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self.current_value @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py index 55ec455d8f1..0723e097967 100644 --- a/homeassistant/components/fido/sensor.py +++ b/homeassistant/components/fido/sensor.py @@ -109,12 +109,12 @@ class FidoSensor(SensorEntity): return f"{self.client_name} {self._number} {self._name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/file/sensor.py b/homeassistant/components/file/sensor.py index 5d8a9475235..73b262c9090 100644 --- a/homeassistant/components/file/sensor.py +++ b/homeassistant/components/file/sensor.py @@ -62,7 +62,7 @@ class FileSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement @@ -72,7 +72,7 @@ class FileSensor(SensorEntity): return ICON @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 856b29364ae..dc44d3d8255 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -63,7 +63,7 @@ class Filesize(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the size of the file in MB.""" decimals = 2 state_mb = round(self._size / 1e6, decimals) @@ -84,6 +84,6 @@ class Filesize(SensorEntity): } @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index c40c703b846..f9705887549 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -332,7 +332,7 @@ class SensorFilter(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -342,7 +342,7 @@ class SensorFilter(SensorEntity): return self._icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit_of_measurement of the device.""" return self._unit_of_measurement diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index 9159e0df49a..d584bbed4bb 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -179,8 +179,8 @@ class FinTsAccount(SensorEntity): """Get the current balance and currency for the account.""" bank = self._client.client balance = bank.get_balance(self._account) - self._attr_state = balance.amount.amount - self._attr_unit_of_measurement = balance.amount.currency + self._attr_native_value = balance.amount.amount + self._attr_native_unit_of_measurement = balance.amount.currency _LOGGER.debug("updated balance of account %s", self.name) @@ -198,13 +198,13 @@ class FinTsHoldingsAccount(SensorEntity): self._account = account self._holdings: list[Any] = [] self._attr_icon = ICON - self._attr_unit_of_measurement = "EUR" + self._attr_native_unit_of_measurement = "EUR" def update(self) -> None: """Get the current holdings for the account.""" bank = self._client.client self._holdings = bank.get_holdings(self._account) - self._attr_state = sum(h.total_value for h in self._holdings) + self._attr_native_value = sum(h.total_value for h in self._holdings) @property def extra_state_attributes(self) -> dict: diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py index 58b3239331c..ec446621212 100644 --- a/homeassistant/components/fireservicerota/sensor.py +++ b/homeassistant/components/fireservicerota/sensor.py @@ -49,7 +49,7 @@ class IncidentsSensor(RestoreEntity, SensorEntity): return "mdi:fire-truck" @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/firmata/sensor.py b/homeassistant/components/firmata/sensor.py index fedac6f76d9..b46e96f3c25 100644 --- a/homeassistant/components/firmata/sensor.py +++ b/homeassistant/components/firmata/sensor.py @@ -54,6 +54,6 @@ class FirmataSensor(FirmataPinEntity, SensorEntity): await self._api.stop_pin() @property - def state(self) -> int: + def native_value(self) -> int: """Return sensor state.""" return self._api.state diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 9f99b3d0bb0..0bd4ed36199 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -374,12 +374,12 @@ class FitbitSensor(SensorEntity): return self._name @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/fixer/sensor.py b/homeassistant/components/fixer/sensor.py index 9214dd6907e..3108f7d3272 100644 --- a/homeassistant/components/fixer/sensor.py +++ b/homeassistant/components/fixer/sensor.py @@ -64,12 +64,12 @@ class ExchangeRateSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._target @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/flick_electric/sensor.py b/homeassistant/components/flick_electric/sensor.py index ab628e205c7..938507e4b0c 100644 --- a/homeassistant/components/flick_electric/sensor.py +++ b/homeassistant/components/flick_electric/sensor.py @@ -36,7 +36,7 @@ async def async_setup_entry( class FlickPricingSensor(SensorEntity): """Entity object for Flick Electric sensor.""" - _attr_unit_of_measurement = UNIT_NAME + _attr_native_unit_of_measurement = UNIT_NAME def __init__(self, api: FlickAPI) -> None: """Entity object for Flick Electric sensor.""" @@ -53,7 +53,7 @@ class FlickPricingSensor(SensorEntity): return FRIENDLY_NAME @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._price.price diff --git a/homeassistant/components/flo/sensor.py b/homeassistant/components/flo/sensor.py index 0504d451e14..b64ed9ee3e4 100644 --- a/homeassistant/components/flo/sensor.py +++ b/homeassistant/components/flo/sensor.py @@ -61,7 +61,7 @@ class FloDailyUsageSensor(FloEntity, SensorEntity): """Monitors the daily water usage.""" _attr_icon = WATER_ICON - _attr_unit_of_measurement = VOLUME_GALLONS + _attr_native_unit_of_measurement = VOLUME_GALLONS def __init__(self, device): """Initialize the daily water usage sensor.""" @@ -69,7 +69,7 @@ class FloDailyUsageSensor(FloEntity, SensorEntity): self._state: float = None @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the current daily usage.""" if self._device.consumption_today is None: return None @@ -85,7 +85,7 @@ class FloSystemModeSensor(FloEntity, SensorEntity): self._state: str = None @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the current system mode.""" if not self._device.current_system_mode: return None @@ -96,7 +96,7 @@ class FloCurrentFlowRateSensor(FloEntity, SensorEntity): """Monitors the current water flow rate.""" _attr_icon = GAUGE_ICON - _attr_unit_of_measurement = "gpm" + _attr_native_unit_of_measurement = "gpm" def __init__(self, device): """Initialize the flow rate sensor.""" @@ -104,7 +104,7 @@ class FloCurrentFlowRateSensor(FloEntity, SensorEntity): self._state: float = None @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the current flow rate.""" if self._device.current_flow_rate is None: return None @@ -115,7 +115,7 @@ class FloTemperatureSensor(FloEntity, SensorEntity): """Monitors the temperature.""" _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_unit_of_measurement = TEMP_FAHRENHEIT + _attr_native_unit_of_measurement = TEMP_FAHRENHEIT def __init__(self, name, device): """Initialize the temperature sensor.""" @@ -123,7 +123,7 @@ class FloTemperatureSensor(FloEntity, SensorEntity): self._state: float = None @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the current temperature.""" if self._device.temperature is None: return None @@ -134,7 +134,7 @@ class FloHumiditySensor(FloEntity, SensorEntity): """Monitors the humidity.""" _attr_device_class = DEVICE_CLASS_HUMIDITY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, device): """Initialize the humidity sensor.""" @@ -142,7 +142,7 @@ class FloHumiditySensor(FloEntity, SensorEntity): self._state: float = None @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the current humidity.""" if self._device.humidity is None: return None @@ -153,7 +153,7 @@ class FloPressureSensor(FloEntity, SensorEntity): """Monitors the water pressure.""" _attr_device_class = DEVICE_CLASS_PRESSURE - _attr_unit_of_measurement = PRESSURE_PSI + _attr_native_unit_of_measurement = PRESSURE_PSI def __init__(self, device): """Initialize the pressure sensor.""" @@ -161,7 +161,7 @@ class FloPressureSensor(FloEntity, SensorEntity): self._state: float = None @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the current water pressure.""" if self._device.current_psi is None: return None @@ -172,7 +172,7 @@ class FloBatterySensor(FloEntity, SensorEntity): """Monitors the battery level for battery-powered leak detectors.""" _attr_device_class = DEVICE_CLASS_BATTERY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, device): """Initialize the battery sensor.""" @@ -180,6 +180,6 @@ class FloBatterySensor(FloEntity, SensorEntity): self._state: float = None @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the current battery level.""" return self._device.battery_level diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index d890443d238..ee67a863be6 100644 --- a/homeassistant/components/flume/sensor.py +++ b/homeassistant/components/flume/sensor.py @@ -136,7 +136,7 @@ class FlumeSensor(CoordinatorEntity, SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" sensor_key = self._flume_query_sensor[0] if sensor_key not in self._flume_device.values: @@ -145,7 +145,7 @@ class FlumeSensor(CoordinatorEntity, SensorEntity): return _format_state_value(self._flume_device.values[sensor_key]) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" # This is in gallons per SCAN_INTERVAL return self._flume_query_sensor[1]["unit_of_measurement"] diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py index 88fb0147296..e28419c5d06 100644 --- a/homeassistant/components/flunearyou/sensor.py +++ b/homeassistant/components/flunearyou/sensor.py @@ -116,7 +116,7 @@ class FluNearYouSensor(CoordinatorEntity, SensorEntity): f"{entry.data[CONF_LATITUDE]}," f"{entry.data[CONF_LONGITUDE]}_{sensor_type}" ) - self._attr_unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit self._entry = entry self._sensor_type = sensor_type @@ -149,7 +149,7 @@ class CdcSensor(FluNearYouSensor): ATTR_STATE: self.coordinator.data["name"], } ) - self._attr_state = self.coordinator.data[self._sensor_type] + self._attr_native_value = self.coordinator.data[self._sensor_type] class UserSensor(FluNearYouSensor): @@ -181,7 +181,7 @@ class UserSensor(FluNearYouSensor): ] = self.coordinator.data["state"]["last_week_data"][states_key] if self._sensor_type == SENSOR_TYPE_USER_TOTAL: - self._attr_state = sum( + self._attr_native_value = sum( v for k, v in self.coordinator.data["local"].items() if k @@ -194,4 +194,4 @@ class UserSensor(FluNearYouSensor): ) ) else: - self._attr_state = self.coordinator.data["local"][self._sensor_type] + self._attr_native_value = self.coordinator.data["local"][self._sensor_type] diff --git a/homeassistant/components/folder/sensor.py b/homeassistant/components/folder/sensor.py index 707f22f98ba..c7257d40237 100644 --- a/homeassistant/components/folder/sensor.py +++ b/homeassistant/components/folder/sensor.py @@ -79,7 +79,7 @@ class Folder(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" decimals = 2 size_mb = round(self._size / 1e6, decimals) @@ -102,6 +102,6 @@ class Folder(SensorEntity): } @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/foobot/sensor.py b/homeassistant/components/foobot/sensor.py index d635f231818..dd9f086d0d9 100644 --- a/homeassistant/components/foobot/sensor.py +++ b/homeassistant/components/foobot/sensor.py @@ -126,7 +126,7 @@ class FoobotSensor(SensorEntity): return SENSOR_TYPES[self.type][2] @property - def state(self): + def native_value(self): """Return the state of the device.""" try: data = self.foobot_data.data[self.type] @@ -140,7 +140,7 @@ class FoobotSensor(SensorEntity): return f"{self._uuid}_{self.type}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity.""" return self._unit_of_measurement diff --git a/homeassistant/components/forecast_solar/const.py b/homeassistant/components/forecast_solar/const.py index 7ae6fe01d42..ea76ed7da2a 100644 --- a/homeassistant/components/forecast_solar/const.py +++ b/homeassistant/components/forecast_solar/const.py @@ -30,14 +30,14 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( name="Estimated Energy Production - Today", state=lambda estimate: estimate.energy_production_today / 1000, device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + 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, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), ForecastSolarSensorEntityDescription( key="power_highest_peak_time_today", @@ -55,7 +55,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( device_class=DEVICE_CLASS_POWER, state=lambda estimate: estimate.power_production_now, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, ), ForecastSolarSensorEntityDescription( key="power_production_next_hour", @@ -65,7 +65,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( name="Estimated Power Production - Next Hour", device_class=DEVICE_CLASS_POWER, entity_registry_enabled_default=False, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, ), ForecastSolarSensorEntityDescription( key="power_production_next_12hours", @@ -75,7 +75,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( name="Estimated Power Production - Next 12 Hours", device_class=DEVICE_CLASS_POWER, entity_registry_enabled_default=False, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, ), ForecastSolarSensorEntityDescription( key="power_production_next_24hours", @@ -85,20 +85,20 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( name="Estimated Power Production - Next 24 Hours", device_class=DEVICE_CLASS_POWER, entity_registry_enabled_default=False, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, ), ForecastSolarSensorEntityDescription( key="energy_current_hour", name="Estimated Energy Production - This Hour", state=lambda estimate: estimate.energy_current_hour / 1000, device_class=DEVICE_CLASS_ENERGY, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + 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, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), ) diff --git a/homeassistant/components/forecast_solar/sensor.py b/homeassistant/components/forecast_solar/sensor.py index 5d3f440f4b6..29ba14ac463 100644 --- a/homeassistant/components/forecast_solar/sensor.py +++ b/homeassistant/components/forecast_solar/sensor.py @@ -60,7 +60,7 @@ class ForecastSolarSensorEntity(CoordinatorEntity, SensorEntity): } @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the sensor.""" if self.entity_description.state is None: state: StateType | datetime = getattr( diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index e68f7208538..939c53b47db 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -109,12 +109,12 @@ class FreeboxSensor(SensorEntity): return self._name @property - def state(self) -> str: + def native_value(self) -> str: """Return the state.""" return self._state @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit.""" return self._unit diff --git a/homeassistant/components/freedompro/sensor.py b/homeassistant/components/freedompro/sensor.py index 0c12f20849c..e5322924864 100644 --- a/homeassistant/components/freedompro/sensor.py +++ b/homeassistant/components/freedompro/sensor.py @@ -64,8 +64,8 @@ class Device(CoordinatorEntity, SensorEntity): } self._attr_device_class = DEVICE_CLASS_MAP[device["type"]] self._attr_state_class = STATE_CLASS_MAP[device["type"]] - self._attr_unit_of_measurement = UNIT_MAP[device["type"]] - self._attr_state = 0 + self._attr_native_unit_of_measurement = UNIT_MAP[device["type"]] + self._attr_native_value = 0 @callback def _handle_coordinator_update(self) -> None: @@ -80,7 +80,7 @@ class Device(CoordinatorEntity, SensorEntity): ) if device is not None and "state" in device: state = device["state"] - self._attr_state = state[DEVICE_KEY_MAP[self._type]] + self._attr_native_value = state[DEVICE_KEY_MAP[self._type]] super()._handle_coordinator_update() async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index c7d3fc243a5..cbbaa40aaa6 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -290,7 +290,9 @@ class FritzBoxSensor(FritzBoxBaseEntity, SensorEntity): self._attr_icon = self._sensor_data.get("icon") self._attr_name = f"{device_friendly_name} {self._sensor_data['name']}" self._attr_state_class = self._sensor_data.get("state_class") - self._attr_unit_of_measurement = self._sensor_data.get("unit_of_measurement") + self._attr_native_unit_of_measurement = self._sensor_data.get( + "unit_of_measurement" + ) self._attr_unique_id = f"{fritzbox_tools.unique_id}-{sensor_type}" super().__init__(fritzbox_tools, device_friendly_name) @@ -311,7 +313,7 @@ class FritzBoxSensor(FritzBoxBaseEntity, SensorEntity): self._attr_available = False return - self._attr_state = self._last_device_value = self._state_provider( + self._attr_native_value = self._last_device_value = self._state_provider( status, self._last_device_value ) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index cef325a61f3..ce5e74cfeec 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -12,7 +12,6 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_NAME, - ATTR_UNIT_OF_MEASUREMENT, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, @@ -139,7 +138,6 @@ class FritzBoxEntity(CoordinatorEntity): self.ain = ain self._name = entity_info[ATTR_NAME] self._unique_id = entity_info[ATTR_ENTITY_ID] - self._unit_of_measurement = entity_info[ATTR_UNIT_OF_MEASUREMENT] self._device_class = entity_info[ATTR_DEVICE_CLASS] self._attr_state_class = entity_info[ATTR_STATE_CLASS] @@ -174,11 +172,6 @@ class FritzBoxEntity(CoordinatorEntity): """Return the name of the device.""" return self._name - @property - def unit_of_measurement(self) -> str | None: - """Return the unit of measurement.""" - return self._unit_of_measurement - @property def device_class(self) -> str | None: """Return the device class.""" diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 9d78afca4de..01bea17fb3c 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -3,6 +3,8 @@ from __future__ import annotations from datetime import datetime +from pyfritzhome import FritzhomeDevice + from homeassistant.components.sensor import ( ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT, @@ -25,6 +27,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.dt import utc_from_timestamp from . import FritzBoxEntity @@ -34,7 +37,7 @@ from .const import ( CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN, ) -from .model import SensorExtraAttributes +from .model import EntityInfo, SensorExtraAttributes async def async_setup_entry( @@ -106,16 +109,30 @@ async def async_setup_entry( async_add_entities(entities) -class FritzBoxBatterySensor(FritzBoxEntity, SensorEntity): +class FritzBoxSensor(FritzBoxEntity, SensorEntity): + """The entity class for FRITZ!SmartHome sensors.""" + + def __init__( + self, + entity_info: EntityInfo, + coordinator: DataUpdateCoordinator[dict[str, FritzhomeDevice]], + ain: str, + ) -> None: + """Initialize the FritzBox entity.""" + FritzBoxEntity.__init__(self, entity_info, coordinator, ain) + self._attr_native_unit_of_measurement = entity_info[ATTR_UNIT_OF_MEASUREMENT] + + +class FritzBoxBatterySensor(FritzBoxSensor): """The entity class for FRITZ!SmartHome battery sensors.""" @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of the sensor.""" return self.device.battery_level # type: ignore [no-any-return] -class FritzBoxPowerSensor(FritzBoxEntity, SensorEntity): +class FritzBoxPowerSensor(FritzBoxSensor): """The entity class for FRITZ!SmartHome power consumption sensors.""" @property @@ -126,7 +143,7 @@ class FritzBoxPowerSensor(FritzBoxEntity, SensorEntity): return 0.0 -class FritzBoxEnergySensor(FritzBoxEntity, SensorEntity): +class FritzBoxEnergySensor(FritzBoxSensor): """The entity class for FRITZ!SmartHome total energy sensors.""" @property @@ -143,11 +160,11 @@ class FritzBoxEnergySensor(FritzBoxEntity, SensorEntity): return utc_from_timestamp(0) -class FritzBoxTempSensor(FritzBoxEntity, SensorEntity): +class FritzBoxTempSensor(FritzBoxSensor): """The entity class for FRITZ!SmartHome temperature sensors.""" @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the state of the sensor.""" return self.device.temperature # type: ignore [no-any-return] diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py index 63b3cd81aa5..31e04077656 100644 --- a/homeassistant/components/fritzbox_callmonitor/sensor.py +++ b/homeassistant/components/fritzbox_callmonitor/sensor.py @@ -158,7 +158,7 @@ class FritzBoxCallSensor(SensorEntity): return self._fritzbox_phonebook is not None @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 211fdaabafd..68430684d85 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -308,8 +308,8 @@ class FroniusTemplateSensor(SensorEntity): state = self._parent.data.get(self._key) self._attr_state = state.get("value") if isinstance(self._attr_state, float): - self._attr_state = round(self._attr_state, 2) - self._attr_unit_of_measurement = state.get("unit") + self._attr_native_value = round(self._attr_state, 2) + self._attr_native_unit_of_measurement = state.get("unit") @property def last_reset(self) -> dt.dt.datetime | None: diff --git a/homeassistant/components/garages_amsterdam/sensor.py b/homeassistant/components/garages_amsterdam/sensor.py index ed01862aba4..da3a7a4dc24 100644 --- a/homeassistant/components/garages_amsterdam/sensor.py +++ b/homeassistant/components/garages_amsterdam/sensor.py @@ -76,7 +76,7 @@ class GaragesamsterdamSensor(CoordinatorEntity, SensorEntity): ) @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" return getattr(self.coordinator.data[self._garage_name], self._info_type) @@ -86,7 +86,7 @@ class GaragesamsterdamSensor(CoordinatorEntity, SensorEntity): return SENSORS[self._info_type] @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return unit of measurement.""" return "cars" diff --git a/homeassistant/components/gdacs/sensor.py b/homeassistant/components/gdacs/sensor.py index 2e4759088fc..8b4c60046db 100644 --- a/homeassistant/components/gdacs/sensor.py +++ b/homeassistant/components/gdacs/sensor.py @@ -105,7 +105,7 @@ class GdacsSensor(SensorEntity): self._removed = status_info.removed @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._total @@ -125,7 +125,7 @@ class GdacsSensor(SensorEntity): return DEFAULT_ICON @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return DEFAULT_UNIT_OF_MEASUREMENT diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index 0c96ec595b6..362e729f57a 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -79,12 +79,12 @@ class GeniusBattery(GeniusDevice, SensorEntity): return DEVICE_CLASS_BATTERY @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit of measurement of the sensor.""" return PERCENTAGE @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" level = self._device.data["state"][self._state_attr] return level if level != 255 else 0 @@ -105,7 +105,7 @@ class GeniusIssue(GeniusEntity, SensorEntity): self._issues = [] @property - def state(self) -> str: + def native_value(self) -> str: """Return the number of issues.""" return len(self._issues) diff --git a/homeassistant/components/geo_rss_events/sensor.py b/homeassistant/components/geo_rss_events/sensor.py index df5f11850fd..f5797121603 100644 --- a/homeassistant/components/geo_rss_events/sensor.py +++ b/homeassistant/components/geo_rss_events/sensor.py @@ -121,12 +121,12 @@ class GeoRssServiceSensor(SensorEntity): return f"{self._service_name} {'Any' if self._category is None else self._category}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement diff --git a/homeassistant/components/geonetnz_quakes/sensor.py b/homeassistant/components/geonetnz_quakes/sensor.py index 94c7965663a..605f56b1272 100644 --- a/homeassistant/components/geonetnz_quakes/sensor.py +++ b/homeassistant/components/geonetnz_quakes/sensor.py @@ -106,7 +106,7 @@ class GeonetnzQuakesSensor(SensorEntity): self._removed = status_info.removed @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._total @@ -126,7 +126,7 @@ class GeonetnzQuakesSensor(SensorEntity): return DEFAULT_ICON @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return DEFAULT_UNIT_OF_MEASUREMENT diff --git a/homeassistant/components/geonetnz_volcano/sensor.py b/homeassistant/components/geonetnz_volcano/sensor.py index c0cc6801437..fc9f0f30b2c 100644 --- a/homeassistant/components/geonetnz_volcano/sensor.py +++ b/homeassistant/components/geonetnz_volcano/sensor.py @@ -130,7 +130,7 @@ class GeonetnzVolcanoSensor(SensorEntity): ) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._alert_level @@ -145,7 +145,7 @@ class GeonetnzVolcanoSensor(SensorEntity): return f"Volcano {self._title}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return "alert level" diff --git a/homeassistant/components/gios/const.py b/homeassistant/components/gios/const.py index 9b890442166..fb96a08ab5b 100644 --- a/homeassistant/components/gios/const.py +++ b/homeassistant/components/gios/const.py @@ -41,43 +41,43 @@ SENSOR_TYPES: Final[tuple[GiosSensorEntityDescription, ...]] = ( GiosSensorEntityDescription( key=ATTR_C6H6, name="C6H6", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_CO, name="CO", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_NO2, name="NO2", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_O3, name="O3", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_PM10, name="PM10", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_PM25, name="PM2.5", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_SO2, name="SO2", - unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), ) diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index b651112b9db..c58f08965ec 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -107,7 +107,7 @@ class GiosSensor(CoordinatorEntity, SensorEntity): return self._attrs @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state.""" state = getattr(self.coordinator.data, self.entity_description.key).value assert self.entity_description.value is not None @@ -118,7 +118,7 @@ class GiosAqiSensor(GiosSensor): """Define an GIOS AQI sensor.""" @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state.""" return cast( StateType, getattr(self.coordinator.data, self.entity_description.key).value diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index c7812fa621d..fb7a0167d8a 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -108,7 +108,7 @@ class GitHubSensor(SensorEntity): return self._unique_id @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/gitlab_ci/sensor.py b/homeassistant/components/gitlab_ci/sensor.py index 0b619853348..e63e07d6c85 100644 --- a/homeassistant/components/gitlab_ci/sensor.py +++ b/homeassistant/components/gitlab_ci/sensor.py @@ -88,7 +88,7 @@ class GitLabSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/gitter/sensor.py b/homeassistant/components/gitter/sensor.py index 20b68b2e5a9..9e13e155f27 100644 --- a/homeassistant/components/gitter/sensor.py +++ b/homeassistant/components/gitter/sensor.py @@ -65,12 +65,12 @@ class GitterSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py index b74662db22b..491dd297a05 100644 --- a/homeassistant/components/glances/const.py +++ b/homeassistant/components/glances/const.py @@ -44,154 +44,154 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( key="disk_use_percent", type="fs", name_suffix="used percent", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:harddisk", ), GlancesSensorEntityDescription( key="disk_use", type="fs", name_suffix="used", - unit_of_measurement=DATA_GIBIBYTES, + native_unit_of_measurement=DATA_GIBIBYTES, icon="mdi:harddisk", ), GlancesSensorEntityDescription( key="disk_free", type="fs", name_suffix="free", - unit_of_measurement=DATA_GIBIBYTES, + native_unit_of_measurement=DATA_GIBIBYTES, icon="mdi:harddisk", ), GlancesSensorEntityDescription( key="memory_use_percent", type="mem", name_suffix="RAM used percent", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:memory", ), GlancesSensorEntityDescription( key="memory_use", type="mem", name_suffix="RAM used", - unit_of_measurement=DATA_MEBIBYTES, + native_unit_of_measurement=DATA_MEBIBYTES, icon="mdi:memory", ), GlancesSensorEntityDescription( key="memory_free", type="mem", name_suffix="RAM free", - unit_of_measurement=DATA_MEBIBYTES, + native_unit_of_measurement=DATA_MEBIBYTES, icon="mdi:memory", ), GlancesSensorEntityDescription( key="swap_use_percent", type="memswap", name_suffix="Swap used percent", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:memory", ), GlancesSensorEntityDescription( key="swap_use", type="memswap", name_suffix="Swap used", - unit_of_measurement=DATA_GIBIBYTES, + native_unit_of_measurement=DATA_GIBIBYTES, icon="mdi:memory", ), GlancesSensorEntityDescription( key="swap_free", type="memswap", name_suffix="Swap free", - unit_of_measurement=DATA_GIBIBYTES, + native_unit_of_measurement=DATA_GIBIBYTES, icon="mdi:memory", ), GlancesSensorEntityDescription( key="processor_load", type="load", name_suffix="CPU load", - unit_of_measurement="15 min", + native_unit_of_measurement="15 min", icon=CPU_ICON, ), GlancesSensorEntityDescription( key="process_running", type="processcount", name_suffix="Running", - unit_of_measurement="Count", + native_unit_of_measurement="Count", icon=CPU_ICON, ), GlancesSensorEntityDescription( key="process_total", type="processcount", name_suffix="Total", - unit_of_measurement="Count", + native_unit_of_measurement="Count", icon=CPU_ICON, ), GlancesSensorEntityDescription( key="process_thread", type="processcount", name_suffix="Thread", - unit_of_measurement="Count", + native_unit_of_measurement="Count", icon=CPU_ICON, ), GlancesSensorEntityDescription( key="process_sleeping", type="processcount", name_suffix="Sleeping", - unit_of_measurement="Count", + native_unit_of_measurement="Count", icon=CPU_ICON, ), GlancesSensorEntityDescription( key="cpu_use_percent", type="cpu", name_suffix="CPU used", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon=CPU_ICON, ), GlancesSensorEntityDescription( key="temperature_core", type="sensors", name_suffix="Temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, ), GlancesSensorEntityDescription( key="temperature_hdd", type="sensors", name_suffix="Temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, ), GlancesSensorEntityDescription( key="fan_speed", type="sensors", name_suffix="Fan speed", - unit_of_measurement="RPM", + native_unit_of_measurement="RPM", icon="mdi:fan", ), GlancesSensorEntityDescription( key="battery", type="sensors", name_suffix="Charge", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:battery", ), GlancesSensorEntityDescription( key="docker_active", type="docker", name_suffix="Containers active", - unit_of_measurement="", + native_unit_of_measurement="", icon="mdi:docker", ), GlancesSensorEntityDescription( key="docker_cpu_use", type="docker", name_suffix="Containers CPU used", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, icon="mdi:docker", ), GlancesSensorEntityDescription( key="docker_memory_use", type="docker", name_suffix="Containers RAM used", - unit_of_measurement=DATA_MEBIBYTES, + native_unit_of_measurement=DATA_MEBIBYTES, icon="mdi:docker", ), ) diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index fd31ee37faf..76e2a1c617a 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -83,7 +83,7 @@ class GlancesSensor(SensorEntity): return self.glances_data.available @property - def state(self): + def native_value(self): """Return the state of the resources.""" return self._state diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py index 594e1f0046b..31eadd55969 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -48,12 +48,12 @@ class YetiSensor(YetiEntity): self._attr_entity_registry_enabled_default = sensor.get(ATTR_DEFAULT_ENABLED) self._attr_last_reset = sensor.get(ATTR_LAST_RESET) self._attr_name = f"{name} {sensor.get(ATTR_NAME)}" + self._attr_native_unit_of_measurement = sensor.get(ATTR_UNIT_OF_MEASUREMENT) self._attr_state_class = sensor.get(ATTR_STATE_CLASS) self._attr_unique_id = f"{server_unique_id}/{sensor_name}" - self._attr_unit_of_measurement = sensor.get(ATTR_UNIT_OF_MEASUREMENT) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state.""" if self.api.data: return self.api.data.get(self._condition) diff --git a/homeassistant/components/gogogate2/sensor.py b/homeassistant/components/gogogate2/sensor.py index 99edc855733..a9be18d06a6 100644 --- a/homeassistant/components/gogogate2/sensor.py +++ b/homeassistant/components/gogogate2/sensor.py @@ -72,7 +72,7 @@ class DoorSensorBattery(GoGoGate2Entity, SensorEntity): return DEVICE_CLASS_BATTERY @property - def state(self): + def native_value(self): """Return the state of the entity.""" door = self._get_door() return door.voltage # This is a percentage, not an absolute voltage @@ -110,13 +110,13 @@ class DoorSensorTemperature(GoGoGate2Entity, SensorEntity): return DEVICE_CLASS_TEMPERATURE @property - def state(self): + def native_value(self): """Return the state of the entity.""" door = self._get_door() return door.temperature @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit_of_measurement.""" return TEMP_CELSIUS diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 6dbe6aa698b..c8cb9d54510 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -196,7 +196,7 @@ class GoogleTravelTimeSensor(SensorEntity): await self.first_update() @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._matrix is None: return None @@ -250,7 +250,7 @@ class GoogleTravelTimeSensor(SensorEntity): return res @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/google_wifi/sensor.py b/homeassistant/components/google_wifi/sensor.py index 28ec5df7486..4a062edaae2 100644 --- a/homeassistant/components/google_wifi/sensor.py +++ b/homeassistant/components/google_wifi/sensor.py @@ -95,7 +95,7 @@ class GoogleWifiSensor(SensorEntity): return self._var_icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._var_units @@ -105,7 +105,7 @@ class GoogleWifiSensor(SensorEntity): return self._api.available @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/gpsd/sensor.py b/homeassistant/components/gpsd/sensor.py index 2f97f62337c..1b502827996 100644 --- a/homeassistant/components/gpsd/sensor.py +++ b/homeassistant/components/gpsd/sensor.py @@ -84,7 +84,7 @@ class GpsdSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of GPSD.""" if self.agps_thread.data_stream.mode == 3: return "3D Fix" diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index fac11395c8b..7fbfa717229 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -147,7 +147,7 @@ class CurrentSensor(GEMSensor): """Entity showing power usage on one channel of the monitor.""" _attr_icon = CURRENT_SENSOR_ICON - _attr_unit_of_measurement = UNIT_WATTS + _attr_native_unit_of_measurement = UNIT_WATTS def __init__(self, monitor_serial_number, number, name, net_metering): """Construct the entity.""" @@ -158,7 +158,7 @@ class CurrentSensor(GEMSensor): return monitor.channels[self._number - 1] @property - def state(self): + def native_value(self): """Return the current number of watts being used by the channel.""" if not self._sensor: return None @@ -203,7 +203,7 @@ class PulseCounter(GEMSensor): return monitor.pulse_counters[self._number - 1] @property - def state(self): + def native_value(self): """Return the current rate of change for the given pulse counter.""" if not self._sensor or self._sensor.pulses_per_second is None: return None @@ -225,7 +225,7 @@ class PulseCounter(GEMSensor): return 3600 @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement for this pulse counter.""" return f"{self._counted_quantity}/{self._time_unit}" @@ -253,7 +253,7 @@ class TemperatureSensor(GEMSensor): return monitor.temperature_sensors[self._number - 1] @property - def state(self): + def native_value(self): """Return the current temperature being reported by this sensor.""" if not self._sensor: return None @@ -261,7 +261,7 @@ class TemperatureSensor(GEMSensor): return self._sensor.temperature @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement for this sensor (user specified).""" return self._unit @@ -270,7 +270,7 @@ class VoltageSensor(GEMSensor): """Entity showing voltage.""" _attr_icon = VOLTAGE_ICON - _attr_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT + _attr_native_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT def __init__(self, monitor_serial_number, number, name): """Construct the entity.""" @@ -281,7 +281,7 @@ class VoltageSensor(GEMSensor): return monitor @property - def state(self): + def native_value(self): """Return the current voltage being reported by this sensor.""" if not self._sensor: return None diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 6eb225e7535..671631c5406 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -60,40 +60,40 @@ TOTAL_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="total_money_today", name="Total money today", api_key="plantMoneyText", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), GrowattSensorEntityDescription( key="total_money_total", name="Money lifetime", api_key="totalMoneyText", - unit_of_measurement=CURRENCY_EURO, + native_unit_of_measurement=CURRENCY_EURO, ), GrowattSensorEntityDescription( key="total_energy_today", name="Energy Today", api_key="todayEnergy", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="total_output_power", name="Output Power", api_key="invTodayPpv", - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, ), GrowattSensorEntityDescription( key="total_energy_output", name="Lifetime energy output", api_key="totalEnergy", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="total_maximum_output", name="Maximum power", api_key="nominalPower", - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, ), ) @@ -103,7 +103,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="inverter_energy_today", name="Energy today", api_key="powerToday", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, precision=1, ), @@ -111,7 +111,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="inverter_energy_total", name="Lifetime energy output", api_key="powerTotal", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, precision=1, ), @@ -119,7 +119,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="inverter_voltage_input_1", name="Input 1 voltage", api_key="vpv1", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, precision=2, ), @@ -127,7 +127,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="inverter_amperage_input_1", name="Input 1 Amperage", api_key="ipv1", - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=DEVICE_CLASS_CURRENT, precision=1, ), @@ -135,7 +135,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="inverter_wattage_input_1", name="Input 1 Wattage", api_key="ppv1", - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, precision=1, ), @@ -143,7 +143,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="inverter_voltage_input_2", name="Input 2 voltage", api_key="vpv2", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, precision=1, ), @@ -151,7 +151,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="inverter_amperage_input_2", name="Input 2 Amperage", api_key="ipv2", - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=DEVICE_CLASS_CURRENT, precision=1, ), @@ -159,7 +159,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="inverter_wattage_input_2", name="Input 2 Wattage", api_key="ppv2", - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, precision=1, ), @@ -167,7 +167,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="inverter_voltage_input_3", name="Input 3 voltage", api_key="vpv3", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, precision=1, ), @@ -175,7 +175,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="inverter_amperage_input_3", name="Input 3 Amperage", api_key="ipv3", - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=DEVICE_CLASS_CURRENT, precision=1, ), @@ -183,7 +183,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="inverter_wattage_input_3", name="Input 3 Wattage", api_key="ppv3", - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, precision=1, ), @@ -191,7 +191,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="inverter_internal_wattage", name="Internal wattage", api_key="ppv", - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, precision=1, ), @@ -199,7 +199,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="inverter_reactive_voltage", name="Reactive voltage", api_key="vacr", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, precision=1, ), @@ -207,7 +207,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="inverter_inverter_reactive_amperage", name="Reactive amperage", api_key="iacr", - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=DEVICE_CLASS_CURRENT, precision=1, ), @@ -215,14 +215,14 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="inverter_frequency", name="AC frequency", api_key="fac", - unit_of_measurement=FREQUENCY_HERTZ, + native_unit_of_measurement=FREQUENCY_HERTZ, precision=1, ), GrowattSensorEntityDescription( key="inverter_current_wattage", name="Output power", api_key="pac", - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, precision=1, ), @@ -230,7 +230,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="inverter_current_reactive_wattage", name="Reactive wattage", api_key="pacr", - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, precision=1, ), @@ -238,7 +238,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="inverter_ipm_temperature", name="Intelligent Power Management temperature", api_key="ipmTemperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, precision=1, ), @@ -246,7 +246,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="inverter_temperature", name="Temperature", api_key="temperature", - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, precision=1, ), @@ -257,118 +257,118 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="storage_storage_production_today", name="Storage production today", api_key="eBatDisChargeToday", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="storage_storage_production_lifetime", name="Lifetime Storage production", api_key="eBatDisChargeTotal", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="storage_grid_discharge_today", name="Grid discharged today", api_key="eacDisChargeToday", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="storage_load_consumption_today", name="Load consumption today", api_key="eopDischrToday", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="storage_load_consumption_lifetime", name="Lifetime load consumption", api_key="eopDischrTotal", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="storage_grid_charged_today", name="Grid charged today", api_key="eacChargeToday", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="storage_charge_storage_lifetime", name="Lifetime storaged charged", api_key="eChargeTotal", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="storage_solar_production", name="Solar power production", api_key="ppv", - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, ), GrowattSensorEntityDescription( key="storage_battery_percentage", name="Battery percentage", api_key="capacity", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_BATTERY, ), GrowattSensorEntityDescription( key="storage_power_flow", name="Storage charging/ discharging(-ve)", api_key="pCharge", - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, ), GrowattSensorEntityDescription( key="storage_load_consumption_solar_storage", name="Load consumption(Solar + Storage)", api_key="rateVA", - unit_of_measurement="VA", + native_unit_of_measurement="VA", ), GrowattSensorEntityDescription( key="storage_charge_today", name="Charge today", api_key="eChargeToday", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="storage_import_from_grid", name="Import from grid", api_key="pAcInPut", - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, ), GrowattSensorEntityDescription( key="storage_import_from_grid_today", name="Import from grid today", api_key="eToUserToday", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="storage_import_from_grid_total", name="Import from grid total", api_key="eToUserTotal", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="storage_load_consumption", name="Load consumption", api_key="outPutPower", - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, ), GrowattSensorEntityDescription( key="storage_grid_voltage", name="AC input voltage", api_key="vGrid", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, precision=2, ), @@ -376,7 +376,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="storage_pv_charging_voltage", name="PV charging voltage", api_key="vpv", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, precision=2, ), @@ -384,14 +384,14 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="storage_ac_input_frequency_out", name="AC input frequency", api_key="freqOutPut", - unit_of_measurement=FREQUENCY_HERTZ, + native_unit_of_measurement=FREQUENCY_HERTZ, precision=2, ), GrowattSensorEntityDescription( key="storage_output_voltage", name="Output voltage", api_key="outPutVolt", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, precision=2, ), @@ -399,14 +399,14 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="storage_ac_output_frequency", name="Ac output frequency", api_key="freqGrid", - unit_of_measurement=FREQUENCY_HERTZ, + native_unit_of_measurement=FREQUENCY_HERTZ, precision=2, ), GrowattSensorEntityDescription( key="storage_current_PV", name="Solar charge current", api_key="iAcCharge", - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=DEVICE_CLASS_CURRENT, precision=2, ), @@ -414,7 +414,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="storage_current_1", name="Solar current to storage", api_key="iChargePV1", - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=DEVICE_CLASS_CURRENT, precision=2, ), @@ -422,7 +422,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="storage_grid_amperage_input", name="Grid charge current", api_key="chgCurr", - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=DEVICE_CLASS_CURRENT, precision=2, ), @@ -430,7 +430,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="storage_grid_out_current", name="Grid out current", api_key="outPutCurrent", - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=DEVICE_CLASS_CURRENT, precision=2, ), @@ -438,7 +438,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="storage_battery_voltage", name="Battery voltage", api_key="vBat", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, precision=2, ), @@ -446,7 +446,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="storage_load_percentage", name="Load percentage", api_key="loadPercent", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_BATTERY, precision=2, ), @@ -458,77 +458,77 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="mix_statement_of_charge", name="Statement of charge", api_key="capacity", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_BATTERY, ), GrowattSensorEntityDescription( key="mix_battery_charge_today", name="Battery charged today", api_key="eBatChargeToday", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="mix_battery_charge_lifetime", name="Lifetime battery charged", api_key="eBatChargeTotal", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="mix_battery_discharge_today", name="Battery discharged today", api_key="eBatDisChargeToday", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="mix_battery_discharge_lifetime", name="Lifetime battery discharged", api_key="eBatDisChargeTotal", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="mix_solar_generation_today", name="Solar energy today", api_key="epvToday", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="mix_solar_generation_lifetime", name="Lifetime solar energy", api_key="epvTotal", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="mix_battery_discharge_w", name="Battery discharging W", api_key="pDischarge1", - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, ), GrowattSensorEntityDescription( key="mix_battery_voltage", name="Battery voltage", api_key="vbat", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, ), GrowattSensorEntityDescription( key="mix_pv1_voltage", name="PV1 voltage", api_key="vpv1", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, ), GrowattSensorEntityDescription( key="mix_pv2_voltage", name="PV2 voltage", api_key="vpv2", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, ), # Values from 'mix_totals' API call @@ -536,28 +536,28 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="mix_load_consumption_today", name="Load consumption today", api_key="elocalLoadToday", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="mix_load_consumption_lifetime", name="Lifetime load consumption", api_key="elocalLoadTotal", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="mix_export_to_grid_today", name="Export to grid today", api_key="etoGridToday", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="mix_export_to_grid_lifetime", name="Lifetime export to grid", api_key="etogridTotal", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), # Values from 'mix_system_status' API call @@ -565,63 +565,63 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="mix_battery_charge", name="Battery charging", api_key="chargePower", - unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_KILO_WATT, device_class=DEVICE_CLASS_POWER, ), GrowattSensorEntityDescription( key="mix_load_consumption", name="Load consumption", api_key="pLocalLoad", - unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_KILO_WATT, device_class=DEVICE_CLASS_POWER, ), GrowattSensorEntityDescription( key="mix_wattage_pv_1", name="PV1 Wattage", api_key="pPv1", - unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_KILO_WATT, device_class=DEVICE_CLASS_POWER, ), GrowattSensorEntityDescription( key="mix_wattage_pv_2", name="PV2 Wattage", api_key="pPv2", - unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_KILO_WATT, device_class=DEVICE_CLASS_POWER, ), GrowattSensorEntityDescription( key="mix_wattage_pv_all", name="All PV Wattage", api_key="ppv", - unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_KILO_WATT, device_class=DEVICE_CLASS_POWER, ), GrowattSensorEntityDescription( key="mix_export_to_grid", name="Export to grid", api_key="pactogrid", - unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_KILO_WATT, device_class=DEVICE_CLASS_POWER, ), GrowattSensorEntityDescription( key="mix_import_from_grid", name="Import from grid", api_key="pactouser", - unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_KILO_WATT, device_class=DEVICE_CLASS_POWER, ), GrowattSensorEntityDescription( key="mix_battery_discharge_kw", name="Battery discharging kW", api_key="pdisCharge1", - unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_KILO_WATT, device_class=DEVICE_CLASS_POWER, ), GrowattSensorEntityDescription( key="mix_grid_voltage", name="Grid voltage", api_key="vAc1", - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=DEVICE_CLASS_VOLTAGE, ), # Values from 'mix_detail' API call @@ -629,35 +629,35 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="mix_system_production_today", name="System production today (self-consumption + export)", api_key="eCharge", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="mix_load_consumption_solar_today", name="Load consumption today (solar)", api_key="eChargeToday", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="mix_self_consumption_today", name="Self consumption today (solar + battery)", api_key="eChargeToday1", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="mix_load_consumption_battery_today", name="Load consumption today (battery)", api_key="echarge1", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), GrowattSensorEntityDescription( key="mix_import_from_grid_today", name="Import from grid today (load)", api_key="etouser", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), # This sensor is manually created using the most recent X-Axis value from the chartData @@ -665,7 +665,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="mix_last_update", name="Last Data Update", api_key="lastdataupdate", - unit_of_measurement=None, + native_unit_of_measurement=None, device_class=DEVICE_CLASS_TIMESTAMP, ), # Values from 'dashboard_data' API call @@ -673,7 +673,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="mix_import_from_grid_today_combined", 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 - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), ) @@ -774,7 +774,7 @@ class GrowattInverter(SensorEntity): self._attr_icon = "mdi:solar-power" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" result = self.probe.get_data(self.entity_description.api_key) if self.entity_description.precision is not None: diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index 812e6a58f28..f8f89b1ea36 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -559,7 +559,7 @@ class GTFSDepartureSensor(SensorEntity): return self._name @property - def state(self) -> str | None: # type: ignore + def native_value(self) -> str | None: # type: ignore """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index 2d7cde86cca..ed3cfedba0e 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -126,15 +126,15 @@ class PairedSensorSensor(PairedSensorEntity, SensorEntity): """Initialize.""" super().__init__(entry, coordinator, kind, name, device_class, icon) - self._attr_unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit @callback def _async_update_from_latest_data(self) -> None: """Update the entity.""" if self._kind == SENSOR_KIND_BATTERY: - self._attr_state = self.coordinator.data["battery"] + self._attr_native_value = self.coordinator.data["battery"] elif self._kind == SENSOR_KIND_TEMPERATURE: - self._attr_state = self.coordinator.data["temperature"] + self._attr_native_value = self.coordinator.data["temperature"] class ValveControllerSensor(ValveControllerEntity, SensorEntity): @@ -153,7 +153,7 @@ class ValveControllerSensor(ValveControllerEntity, SensorEntity): """Initialize.""" super().__init__(entry, coordinators, kind, name, device_class, icon) - self._attr_unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit async def _async_continue_entity_setup(self) -> None: """Register API interest (and related tasks) when the entity is added.""" @@ -167,11 +167,13 @@ class ValveControllerSensor(ValveControllerEntity, SensorEntity): self._attr_available = self.coordinators[ API_SYSTEM_ONBOARD_SENSOR_STATUS ].last_update_success - self._attr_state = self.coordinators[API_SYSTEM_ONBOARD_SENSOR_STATUS].data[ - "temperature" - ] + self._attr_native_value = self.coordinators[ + API_SYSTEM_ONBOARD_SENSOR_STATUS + ].data["temperature"] elif self._kind == SENSOR_KIND_UPTIME: self._attr_available = self.coordinators[ API_SYSTEM_DIAGNOSTICS ].last_update_success - self._attr_state = self.coordinators[API_SYSTEM_DIAGNOSTICS].data["uptime"] + self._attr_native_value = self.coordinators[API_SYSTEM_DIAGNOSTICS].data[ + "uptime" + ] diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index 52748ddadad..eb42426e8ea 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -155,12 +155,12 @@ class HabitipySensor(SensorEntity): return f"{DOMAIN}_{self._name}_{self._sensor_name}" @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._sensor_type.unit @@ -195,7 +195,7 @@ class HabitipyTaskSensor(SensorEntity): return f"{DOMAIN}_{self._name}_{self._task_name}" @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state @@ -220,6 +220,6 @@ class HabitipyTaskSensor(SensorEntity): return attrs @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._task_type.unit diff --git a/homeassistant/components/hassio/sensor.py b/homeassistant/components/hassio/sensor.py index e81980d78e1..c0c3e63715c 100644 --- a/homeassistant/components/hassio/sensor.py +++ b/homeassistant/components/hassio/sensor.py @@ -39,7 +39,7 @@ class HassioAddonSensor(HassioAddonEntity, SensorEntity): """Sensor to track a Hass.io add-on attribute.""" @property - def state(self) -> str: + def native_value(self) -> str: """Return state of entity.""" return self.addon_info[self.attribute_name] @@ -48,6 +48,6 @@ class HassioOSSensor(HassioOSEntity, SensorEntity): """Sensor to track a Hass.io add-on attribute.""" @property - def state(self) -> str: + def native_value(self) -> str: """Return state of entity.""" return self.os_info[self.attribute_name] diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py index 55b369c2fde..738837989b9 100644 --- a/homeassistant/components/haveibeenpwned/sensor.py +++ b/homeassistant/components/haveibeenpwned/sensor.py @@ -69,12 +69,12 @@ class HaveIBeenPwnedSensor(SensorEntity): return f"Breaches {self._email}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/hddtemp/sensor.py b/homeassistant/components/hddtemp/sensor.py index 8169fa811e0..49d1c2f28fa 100644 --- a/homeassistant/components/hddtemp/sensor.py +++ b/homeassistant/components/hddtemp/sensor.py @@ -78,7 +78,7 @@ class HddTempSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state @@ -88,7 +88,7 @@ class HddTempSensor(SensorEntity): return DEVICE_CLASS_TEMPERATURE @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 11fd19bd895..7606a2772d6 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -256,7 +256,7 @@ class HERETravelTimeSensor(SensorEntity): ) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" if self._here_data.traffic_mode and self._here_data.traffic_time is not None: return str(round(self._here_data.traffic_time / 60)) @@ -292,7 +292,7 @@ class HERETravelTimeSensor(SensorEntity): return res @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index e8ff9afc4e3..0db311b0354 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -153,7 +153,7 @@ class HistoryStatsSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.value is None or self.count is None: return None @@ -168,7 +168,7 @@ class HistoryStatsSensor(SensorEntity): return self.count @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index f21afc51801..5ea81bff123 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -57,7 +57,7 @@ class HiveSensorEntity(HiveEntity, SensorEntity): return DEVICETYPE[self.device["hiveType"]].get("type") @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return DEVICETYPE[self.device["hiveType"]].get("unit") @@ -67,7 +67,7 @@ class HiveSensorEntity(HiveEntity, SensorEntity): return self.device["haName"] @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self.device["status"]["state"] diff --git a/homeassistant/components/home_connect/sensor.py b/homeassistant/components/home_connect/sensor.py index 463de6cda51..373ad6be295 100644 --- a/homeassistant/components/home_connect/sensor.py +++ b/homeassistant/components/home_connect/sensor.py @@ -42,7 +42,7 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity): self._sign = sign @property - def state(self): + def native_value(self): """Return true if the binary sensor is on.""" return self._state @@ -83,7 +83,7 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity): _LOGGER.debug("Updated, new state: %s", self._state) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 2de80eefd7e..b599e7263c8 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -79,7 +79,7 @@ class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): """Representation of a Homekit humidity sensor.""" _attr_device_class = DEVICE_CLASS_HUMIDITY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" @@ -96,7 +96,7 @@ class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): return HUMIDITY_ICON @property - def state(self): + def native_value(self): """Return the current humidity.""" return self.service.value(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT) @@ -105,7 +105,7 @@ class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity): """Representation of a Homekit temperature sensor.""" _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_unit_of_measurement = TEMP_CELSIUS + _attr_native_unit_of_measurement = TEMP_CELSIUS def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" @@ -122,7 +122,7 @@ class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity): return TEMP_C_ICON @property - def state(self): + def native_value(self): """Return the current temperature in Celsius.""" return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT) @@ -131,7 +131,7 @@ class HomeKitLightSensor(HomeKitEntity, SensorEntity): """Representation of a Homekit light level sensor.""" _attr_device_class = DEVICE_CLASS_ILLUMINANCE - _attr_unit_of_measurement = LIGHT_LUX + _attr_native_unit_of_measurement = LIGHT_LUX def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" @@ -148,7 +148,7 @@ class HomeKitLightSensor(HomeKitEntity, SensorEntity): return BRIGHTNESS_ICON @property - def state(self): + def native_value(self): """Return the current light level in lux.""" return self.service.value(CharacteristicsTypes.LIGHT_LEVEL_CURRENT) @@ -157,7 +157,7 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): """Representation of a Homekit Carbon Dioxide sensor.""" _attr_icon = CO2_ICON - _attr_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION + _attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" @@ -169,7 +169,7 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): return f"{super().name} CO2" @property - def state(self): + def native_value(self): """Return the current CO2 level in ppm.""" return self.service.value(CharacteristicsTypes.CARBON_DIOXIDE_LEVEL) @@ -178,7 +178,7 @@ class HomeKitBatterySensor(HomeKitEntity, SensorEntity): """Representation of a Homekit battery sensor.""" _attr_device_class = DEVICE_CLASS_BATTERY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" @@ -229,7 +229,7 @@ class HomeKitBatterySensor(HomeKitEntity, SensorEntity): return self.service.value(CharacteristicsTypes.CHARGING_STATE) == 1 @property - def state(self): + def native_value(self): """Return the current battery level percentage.""" return self.service.value(CharacteristicsTypes.BATTERY_LEVEL) @@ -281,7 +281,7 @@ class SimpleSensor(CharacteristicEntity, SensorEntity): return self._state_class @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return units for the sensor.""" return self._unit @@ -296,7 +296,7 @@ class SimpleSensor(CharacteristicEntity, SensorEntity): return f"{super().name} - {self._name}" @property - def state(self): + def native_value(self): """Return the current sensor value.""" return self._char.value diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index ad62001d5f9..7cfe0ffc944 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -107,7 +107,7 @@ class HMSensor(HMDevice, SensorEntity): """Representation of a HomeMatic sensor.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" # Does a cast exist for this class? name = self._hmdevice.__class__.__name__ @@ -118,7 +118,7 @@ class HMSensor(HMDevice, SensorEntity): return self._hm_get_state() @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return HM_UNIT_HA_CAST.get(self._state) diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 475df8ec2af..df8ed33ded0 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -137,12 +137,12 @@ class HomematicipAccesspointDutyCycle(HomematicipGenericEntity, SensorEntity): return "mdi:access-point-network" @property - def state(self) -> float: + def native_value(self) -> float: """Return the state of the access point.""" return self._device.dutyCycleLevel @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return PERCENTAGE @@ -164,14 +164,14 @@ class HomematicipHeatingThermostat(HomematicipGenericEntity, SensorEntity): return "mdi:radiator" @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the radiator valve.""" if self._device.valveState != ValveState.ADAPTION_DONE: return self._device.valveState return round(self._device.valvePosition * 100) @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return PERCENTAGE @@ -189,12 +189,12 @@ class HomematicipHumiditySensor(HomematicipGenericEntity, SensorEntity): return DEVICE_CLASS_HUMIDITY @property - def state(self) -> int: + def native_value(self) -> int: """Return the state.""" return self._device.humidity @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return PERCENTAGE @@ -212,7 +212,7 @@ class HomematicipTemperatureSensor(HomematicipGenericEntity, SensorEntity): return DEVICE_CLASS_TEMPERATURE @property - def state(self) -> float: + def native_value(self) -> float: """Return the state.""" if hasattr(self._device, "valveActualTemperature"): return self._device.valveActualTemperature @@ -220,7 +220,7 @@ class HomematicipTemperatureSensor(HomematicipGenericEntity, SensorEntity): return self._device.actualTemperature @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return TEMP_CELSIUS @@ -249,7 +249,7 @@ class HomematicipIlluminanceSensor(HomematicipGenericEntity, SensorEntity): return DEVICE_CLASS_ILLUMINANCE @property - def state(self) -> float: + def native_value(self) -> float: """Return the state.""" if hasattr(self._device, "averageIllumination"): return self._device.averageIllumination @@ -257,7 +257,7 @@ class HomematicipIlluminanceSensor(HomematicipGenericEntity, SensorEntity): return self._device.illumination @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return LIGHT_LUX @@ -287,12 +287,12 @@ class HomematicipPowerSensor(HomematicipGenericEntity, SensorEntity): return DEVICE_CLASS_POWER @property - def state(self) -> float: + def native_value(self) -> float: """Return the power consumption value.""" return self._device.currentPowerConsumption @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return POWER_WATT @@ -305,12 +305,12 @@ class HomematicipWindspeedSensor(HomematicipGenericEntity, SensorEntity): super().__init__(hap, device, post="Windspeed") @property - def state(self) -> float: + def native_value(self) -> float: """Return the wind speed value.""" return self._device.windSpeed @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return SPEED_KILOMETERS_PER_HOUR @@ -338,12 +338,12 @@ class HomematicipTodayRainSensor(HomematicipGenericEntity, SensorEntity): super().__init__(hap, device, post="Today Rain") @property - def state(self) -> float: + def native_value(self) -> float: """Return the today's rain value.""" return round(self._device.todayRainCounter, 2) @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return LENGTH_MILLIMETERS @@ -352,7 +352,7 @@ class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity, SensorEnt """Representation of the HomematicIP passage detector delta counter.""" @property - def state(self) -> int: + def native_value(self) -> int: """Return the passage detector delta counter value.""" return self._device.leftRightCounterDelta diff --git a/homeassistant/components/hp_ilo/sensor.py b/homeassistant/components/hp_ilo/sensor.py index 297bfa5264f..5a44a2937e8 100644 --- a/homeassistant/components/hp_ilo/sensor.py +++ b/homeassistant/components/hp_ilo/sensor.py @@ -133,12 +133,12 @@ class HpIloSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of the sensor.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/htu21d/sensor.py b/homeassistant/components/htu21d/sensor.py index ccbe6a31de2..4f93ecbc42d 100644 --- a/homeassistant/components/htu21d/sensor.py +++ b/homeassistant/components/htu21d/sensor.py @@ -98,12 +98,12 @@ class HTU21DSensor(SensorEntity): return self._name @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit of measurement of the sensor.""" return self._unit_of_measurement diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 4340d5912c9..47987e5607e 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -426,7 +426,7 @@ class HuaweiLteSensor(HuaweiLteBaseEntity, SensorEntity): return f"{self.key}.{self.item}" @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return sensor state.""" return self._state @@ -436,7 +436,7 @@ class HuaweiLteSensor(HuaweiLteBaseEntity, SensorEntity): return self.meta.device_class @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return sensor's unit of measurement.""" return self.meta.unit or self._unit diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index a512012bc68..80658fff21e 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -42,10 +42,10 @@ class HueLightLevel(GenericHueGaugeSensorEntity): """The light level sensor entity for a Hue motion sensor device.""" _attr_device_class = DEVICE_CLASS_ILLUMINANCE - _attr_unit_of_measurement = LIGHT_LUX + _attr_native_unit_of_measurement = LIGHT_LUX @property - def state(self): + def native_value(self): """Return the state of the device.""" if self.sensor.lightlevel is None: return None @@ -78,10 +78,10 @@ class HueTemperature(GenericHueGaugeSensorEntity): _attr_device_class = DEVICE_CLASS_TEMPERATURE _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_unit_of_measurement = TEMP_CELSIUS + _attr_native_unit_of_measurement = TEMP_CELSIUS @property - def state(self): + def native_value(self): """Return the state of the device.""" if self.sensor.temperature is None: return None @@ -94,7 +94,7 @@ class HueBattery(GenericHueSensor, SensorEntity): _attr_device_class = DEVICE_CLASS_BATTERY _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE @property def unique_id(self): @@ -102,7 +102,7 @@ class HueBattery(GenericHueSensor, SensorEntity): return f"{self.sensor.uniqueid}-battery" @property - def state(self): + def native_value(self): """Return the state of the battery.""" return self.sensor.battery diff --git a/homeassistant/components/huisbaasje/sensor.py b/homeassistant/components/huisbaasje/sensor.py index 3cda3cdec00..6f18ad27796 100644 --- a/homeassistant/components/huisbaasje/sensor.py +++ b/homeassistant/components/huisbaasje/sensor.py @@ -75,7 +75,7 @@ class HuisbaasjeSensor(CoordinatorEntity, SensorEntity): return self._icon @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self.coordinator.data[self._source_type][self._sensor_type] is not None: return round( @@ -85,7 +85,7 @@ class HuisbaasjeSensor(CoordinatorEntity, SensorEntity): return None @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Return the unit of measurement.""" return self._unit_of_measurement diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py index d66671fe1ea..14501a9c528 100644 --- a/homeassistant/components/hunterdouglas_powerview/sensor.py +++ b/homeassistant/components/hunterdouglas_powerview/sensor.py @@ -50,7 +50,7 @@ class PowerViewShadeBatterySensor(ShadeEntity, SensorEntity): """Representation of an shade battery charge sensor.""" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return PERCENTAGE @@ -70,7 +70,7 @@ class PowerViewShadeBatterySensor(ShadeEntity, SensorEntity): return f"{self._unique_id}_charge" @property - def state(self): + def native_value(self): """Get the current value in percentage.""" return round( self._shade.raw_data[SHADE_BATTERY_LEVEL] / SHADE_BATTERY_LEVEL_MAX * 100 diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py index a3df466da74..8a188f7dde8 100644 --- a/homeassistant/components/hvv_departures/sensor.py +++ b/homeassistant/components/hvv_departures/sensor.py @@ -177,7 +177,7 @@ class HVVDepartureSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index 62108afbded..0e9afb6d729 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -40,12 +40,12 @@ class HydrawiseSensor(HydrawiseEntity, SensorEntity): """A sensor implementation for Hydrawise device.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the units of measurement.""" return DEVICE_MAP[self._sensor_type][ DEVICE_MAP_INDEX.index("UNIT_OF_MEASURE_INDEX") diff --git a/tests/components/fail2ban/test_sensor.py b/tests/components/fail2ban/test_sensor.py index f9c78e14888..0240ffc6d11 100644 --- a/tests/components/fail2ban/test_sensor.py +++ b/tests/components/fail2ban/test_sensor.py @@ -83,6 +83,7 @@ async def test_single_ban(hass): """Test that log is parsed correctly for single ban.""" log_parser = BanLogParser("/test/fail2ban.log") sensor = BanSensor("fail2ban", "jail_one", log_parser) + sensor.hass = hass assert sensor.name == "fail2ban jail_one" mock_fh = mock_open(read_data=fake_log("single_ban")) with patch("homeassistant.components.fail2ban.sensor.open", mock_fh, create=True): @@ -97,6 +98,7 @@ async def test_ipv6_ban(hass): """Test that log is parsed correctly for IPV6 bans.""" log_parser = BanLogParser("/test/fail2ban.log") sensor = BanSensor("fail2ban", "jail_one", log_parser) + sensor.hass = hass assert sensor.name == "fail2ban jail_one" mock_fh = mock_open(read_data=fake_log("ipv6_ban")) with patch("homeassistant.components.fail2ban.sensor.open", mock_fh, create=True): @@ -111,6 +113,7 @@ async def test_multiple_ban(hass): """Test that log is parsed correctly for multiple ban.""" log_parser = BanLogParser("/test/fail2ban.log") sensor = BanSensor("fail2ban", "jail_one", log_parser) + sensor.hass = hass assert sensor.name == "fail2ban jail_one" mock_fh = mock_open(read_data=fake_log("multi_ban")) with patch("homeassistant.components.fail2ban.sensor.open", mock_fh, create=True): @@ -131,6 +134,7 @@ async def test_unban_all(hass): """Test that log is parsed correctly when unbanning.""" log_parser = BanLogParser("/test/fail2ban.log") sensor = BanSensor("fail2ban", "jail_one", log_parser) + sensor.hass = hass assert sensor.name == "fail2ban jail_one" mock_fh = mock_open(read_data=fake_log("unban_all")) with patch("homeassistant.components.fail2ban.sensor.open", mock_fh, create=True): @@ -148,6 +152,7 @@ async def test_unban_one(hass): """Test that log is parsed correctly when unbanning one ip.""" log_parser = BanLogParser("/test/fail2ban.log") sensor = BanSensor("fail2ban", "jail_one", log_parser) + sensor.hass = hass assert sensor.name == "fail2ban jail_one" mock_fh = mock_open(read_data=fake_log("unban_one")) with patch("homeassistant.components.fail2ban.sensor.open", mock_fh, create=True): @@ -166,6 +171,8 @@ async def test_multi_jail(hass): log_parser = BanLogParser("/test/fail2ban.log") sensor1 = BanSensor("fail2ban", "jail_one", log_parser) sensor2 = BanSensor("fail2ban", "jail_two", log_parser) + sensor1.hass = hass + sensor2.hass = hass assert sensor1.name == "fail2ban jail_one" assert sensor2.name == "fail2ban jail_two" mock_fh = mock_open(read_data=fake_log("multi_jail")) @@ -185,6 +192,7 @@ async def test_ban_active_after_update(hass): """Test that ban persists after subsequent update.""" log_parser = BanLogParser("/test/fail2ban.log") sensor = BanSensor("fail2ban", "jail_one", log_parser) + sensor.hass = hass assert sensor.name == "fail2ban jail_one" mock_fh = mock_open(read_data=fake_log("single_ban")) with patch("homeassistant.components.fail2ban.sensor.open", mock_fh, create=True): diff --git a/tests/components/google_wifi/test_sensor.py b/tests/components/google_wifi/test_sensor.py index 6f4b4652e76..9b430fa5fae 100644 --- a/tests/components/google_wifi/test_sensor.py +++ b/tests/components/google_wifi/test_sensor.py @@ -68,7 +68,7 @@ async def test_setup_get(hass, requests_mock): assert_setup_component(6, "sensor") -def setup_api(data, requests_mock): +def setup_api(hass, data, requests_mock): """Set up API with fake data.""" resource = f"http://localhost{google_wifi.ENDPOINT}" now = datetime(1970, month=1, day=1) @@ -84,6 +84,10 @@ def setup_api(data, requests_mock): "units": cond_list[1], "icon": cond_list[2], } + for name in sensor_dict: + sensor = sensor_dict[name]["sensor"] + sensor.hass = hass + return api, sensor_dict @@ -96,7 +100,7 @@ def fake_delay(hass, ha_delay): def test_name(requests_mock): """Test the name.""" - api, sensor_dict = setup_api(MOCK_DATA, requests_mock) + api, sensor_dict = setup_api(None, MOCK_DATA, requests_mock) for name in sensor_dict: sensor = sensor_dict[name]["sensor"] test_name = sensor_dict[name]["name"] @@ -105,7 +109,7 @@ def test_name(requests_mock): def test_unit_of_measurement(requests_mock): """Test the unit of measurement.""" - api, sensor_dict = setup_api(MOCK_DATA, requests_mock) + api, sensor_dict = setup_api(None, MOCK_DATA, requests_mock) for name in sensor_dict: sensor = sensor_dict[name]["sensor"] assert sensor_dict[name]["units"] == sensor.unit_of_measurement @@ -113,7 +117,7 @@ def test_unit_of_measurement(requests_mock): def test_icon(requests_mock): """Test the icon.""" - api, sensor_dict = setup_api(MOCK_DATA, requests_mock) + api, sensor_dict = setup_api(None, MOCK_DATA, requests_mock) for name in sensor_dict: sensor = sensor_dict[name]["sensor"] assert sensor_dict[name]["icon"] == sensor.icon @@ -121,7 +125,7 @@ def test_icon(requests_mock): def test_state(hass, requests_mock): """Test the initial state.""" - api, sensor_dict = setup_api(MOCK_DATA, requests_mock) + api, sensor_dict = setup_api(hass, MOCK_DATA, requests_mock) now = datetime(1970, month=1, day=1) with patch("homeassistant.util.dt.now", return_value=now): for name in sensor_dict: @@ -140,7 +144,7 @@ def test_state(hass, requests_mock): def test_update_when_value_is_none(hass, requests_mock): """Test state gets updated to unknown when sensor returns no data.""" - api, sensor_dict = setup_api(None, requests_mock) + api, sensor_dict = setup_api(hass, None, requests_mock) for name in sensor_dict: sensor = sensor_dict[name]["sensor"] fake_delay(hass, 2) @@ -150,7 +154,7 @@ def test_update_when_value_is_none(hass, requests_mock): def test_update_when_value_changed(hass, requests_mock): """Test state gets updated when sensor returns a new status.""" - api, sensor_dict = setup_api(MOCK_DATA_NEXT, requests_mock) + api, sensor_dict = setup_api(hass, MOCK_DATA_NEXT, requests_mock) now = datetime(1970, month=1, day=1) with patch("homeassistant.util.dt.now", return_value=now): for name in sensor_dict: @@ -173,7 +177,7 @@ def test_update_when_value_changed(hass, requests_mock): def test_when_api_data_missing(hass, requests_mock): """Test state logs an error when data is missing.""" - api, sensor_dict = setup_api(MOCK_DATA_MISSING, requests_mock) + api, sensor_dict = setup_api(hass, MOCK_DATA_MISSING, requests_mock) now = datetime(1970, month=1, day=1) with patch("homeassistant.util.dt.now", return_value=now): for name in sensor_dict: @@ -183,12 +187,12 @@ def test_when_api_data_missing(hass, requests_mock): assert sensor.state == STATE_UNKNOWN -def test_update_when_unavailable(requests_mock): +def test_update_when_unavailable(hass, requests_mock): """Test state updates when Google Wifi unavailable.""" - api, sensor_dict = setup_api(None, requests_mock) + api, sensor_dict = setup_api(hass, None, requests_mock) api.update = Mock( "google_wifi.GoogleWifiAPI.update", - side_effect=update_side_effect(requests_mock), + side_effect=update_side_effect(hass, requests_mock), ) for name in sensor_dict: sensor = sensor_dict[name]["sensor"] @@ -196,8 +200,8 @@ def test_update_when_unavailable(requests_mock): assert sensor.state is None -def update_side_effect(requests_mock): +def update_side_effect(hass, requests_mock): """Mock representation of update function.""" - api, sensor_dict = setup_api(MOCK_DATA, requests_mock) + api, sensor_dict = setup_api(hass, MOCK_DATA, requests_mock) api.data = None api.available = False From e558b3463e39bb0d7edd0bc89ef6176f2a477130 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 12 Aug 2021 17:40:55 +0200 Subject: [PATCH 175/355] Move temperature conversions to sensor base class (6/8) (#54476) * Move temperature conversions to entity base class (6/8) * Fix tests --- homeassistant/components/radarr/sensor.py | 4 +- homeassistant/components/rainbird/sensor.py | 4 +- homeassistant/components/raincloud/sensor.py | 4 +- .../components/rainforest_eagle/sensor.py | 4 +- .../components/rainmachine/sensor.py | 14 +++--- homeassistant/components/random/sensor.py | 4 +- .../components/recollect_waste/sensor.py | 2 +- homeassistant/components/reddit/sensor.py | 2 +- .../components/rejseplanen/sensor.py | 4 +- homeassistant/components/repetier/sensor.py | 8 ++-- homeassistant/components/rest/sensor.py | 4 +- homeassistant/components/rflink/sensor.py | 4 +- homeassistant/components/rfxtrx/sensor.py | 46 +++++++++---------- homeassistant/components/ring/sensor.py | 8 ++-- homeassistant/components/ripple/sensor.py | 4 +- homeassistant/components/risco/sensor.py | 2 +- .../rituals_perfume_genie/sensor.py | 12 ++--- .../components/rmvtransport/sensor.py | 4 +- homeassistant/components/roomba/sensor.py | 4 +- homeassistant/components/rova/sensor.py | 2 +- homeassistant/components/rtorrent/sensor.py | 4 +- homeassistant/components/sabnzbd/sensor.py | 4 +- homeassistant/components/saj/sensor.py | 4 +- homeassistant/components/scrape/sensor.py | 4 +- .../components/screenlogic/sensor.py | 6 +-- homeassistant/components/season/sensor.py | 2 +- homeassistant/components/sense/sensor.py | 22 ++++----- homeassistant/components/sensehat/sensor.py | 4 +- homeassistant/components/serial/sensor.py | 2 +- homeassistant/components/serial_pm/sensor.py | 4 +- .../components/seventeentrack/sensor.py | 6 +-- homeassistant/components/shelly/sensor.py | 12 ++--- homeassistant/components/shodan/sensor.py | 4 +- homeassistant/components/sht31/sensor.py | 6 +-- homeassistant/components/sigfox/sensor.py | 2 +- homeassistant/components/simplisafe/sensor.py | 4 +- homeassistant/components/simulated/sensor.py | 4 +- homeassistant/components/skybeacon/sensor.py | 8 ++-- homeassistant/components/skybell/sensor.py | 2 +- homeassistant/components/sleepiq/sensor.py | 2 +- homeassistant/components/sma/sensor.py | 4 +- homeassistant/components/smappee/sensor.py | 4 +- .../components/smart_meter_texas/sensor.py | 4 +- .../components/smartthings/sensor.py | 6 +-- homeassistant/components/smarttub/sensor.py | 6 +-- homeassistant/components/smarty/sensor.py | 4 +- homeassistant/components/sms/sensor.py | 4 +- homeassistant/components/snmp/sensor.py | 4 +- homeassistant/components/sochain/sensor.py | 4 +- homeassistant/components/solaredge/const.py | 12 ++--- homeassistant/components/solaredge/sensor.py | 16 +++---- .../components/solaredge_local/sensor.py | 4 +- homeassistant/components/solarlog/sensor.py | 4 +- homeassistant/components/solax/sensor.py | 4 +- homeassistant/components/soma/sensor.py | 4 +- homeassistant/components/somfy/sensor.py | 4 +- homeassistant/components/sonarr/sensor.py | 14 +++--- homeassistant/components/sonos/sensor.py | 4 +- .../components/speedtestdotnet/const.py | 6 +-- .../components/speedtestdotnet/sensor.py | 12 +++-- homeassistant/components/sql/sensor.py | 4 +- homeassistant/components/srp_energy/sensor.py | 4 +- tests/components/sleepiq/test_sensor.py | 5 +- tests/components/srp_energy/test_sensor.py | 4 ++ 64 files changed, 199 insertions(+), 188 deletions(-) diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index 407f491f63a..02e898c8e0f 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -127,7 +127,7 @@ class RadarrSensor(SensorEntity): return "{} {}".format("Radarr", self._name) @property - def state(self): + def native_value(self): """Return sensor state.""" return self._state @@ -137,7 +137,7 @@ class RadarrSensor(SensorEntity): return self._available @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of the sensor.""" return self._unit diff --git a/homeassistant/components/rainbird/sensor.py b/homeassistant/components/rainbird/sensor.py index 2158bc5cf97..0f6ad41b4e3 100644 --- a/homeassistant/components/rainbird/sensor.py +++ b/homeassistant/components/rainbird/sensor.py @@ -45,6 +45,6 @@ class RainBirdSensor(SensorEntity): """Get the latest data and updates the states.""" _LOGGER.debug("Updating sensor: %s", self.name) if self.entity_description.key == SENSOR_TYPE_RAINSENSOR: - self._attr_state = self._controller.get_rain_sensor_state() + self._attr_native_value = self._controller.get_rain_sensor_state() elif self.entity_description.key == SENSOR_TYPE_RAINDELAY: - self._attr_state = self._controller.get_rain_delay() + self._attr_native_value = self._controller.get_rain_delay() diff --git a/homeassistant/components/raincloud/sensor.py b/homeassistant/components/raincloud/sensor.py index ee8f68734ad..c550e43285b 100644 --- a/homeassistant/components/raincloud/sensor.py +++ b/homeassistant/components/raincloud/sensor.py @@ -48,12 +48,12 @@ class RainCloudSensor(RainCloudEntity, SensorEntity): """A sensor implementation for raincloud device.""" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the units of measurement.""" return UNIT_OF_MEASUREMENT_MAP.get(self._sensor_type) diff --git a/homeassistant/components/rainforest_eagle/sensor.py b/homeassistant/components/rainforest_eagle/sensor.py index 53e94d2070e..64eb243c15d 100644 --- a/homeassistant/components/rainforest_eagle/sensor.py +++ b/homeassistant/components/rainforest_eagle/sensor.py @@ -131,7 +131,7 @@ class EagleSensor(SensorEntity): self._type = sensor_type sensor_info = SENSORS[sensor_type] self._attr_name = sensor_info.name - self._attr_unit_of_measurement = sensor_info.unit_of_measurement + self._attr_native_unit_of_measurement = sensor_info.unit_of_measurement self._attr_device_class = sensor_info.device_class self._attr_state_class = sensor_info.state_class self._attr_last_reset = sensor_info.last_reset @@ -139,7 +139,7 @@ class EagleSensor(SensorEntity): def update(self): """Get the energy information from the Rainforest Eagle.""" self.eagle_data.update() - self._attr_state = self.eagle_data.get_state(self._type) + self._attr_native_value = self.eagle_data.get_state(self._type) class EagleData: diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 808c6a06bc2..2316b27acbf 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -134,7 +134,7 @@ class RainMachineSensor(RainMachineEntity, SensorEntity): self._attr_entity_registry_enabled_default = enabled_by_default self._attr_icon = icon self._attr_name = name - self._attr_unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit class ProvisionSettingsSensor(RainMachineSensor): @@ -144,7 +144,7 @@ class ProvisionSettingsSensor(RainMachineSensor): def update_from_latest_data(self) -> None: """Update the state.""" if self._entity_type == TYPE_FLOW_SENSOR_CLICK_M3: - self._attr_state = self.coordinator.data["system"].get( + self._attr_native_value = self.coordinator.data["system"].get( "flowSensorClicksPerCubicMeter" ) elif self._entity_type == TYPE_FLOW_SENSOR_CONSUMED_LITERS: @@ -154,15 +154,15 @@ class ProvisionSettingsSensor(RainMachineSensor): ) if clicks and clicks_per_m3: - self._attr_state = (clicks * 1000) / clicks_per_m3 + self._attr_native_value = (clicks * 1000) / clicks_per_m3 else: - self._attr_state = None + self._attr_native_value = None elif self._entity_type == TYPE_FLOW_SENSOR_START_INDEX: - self._attr_state = self.coordinator.data["system"].get( + self._attr_native_value = self.coordinator.data["system"].get( "flowSensorStartIndex" ) elif self._entity_type == TYPE_FLOW_SENSOR_WATERING_CLICKS: - self._attr_state = self.coordinator.data["system"].get( + self._attr_native_value = self.coordinator.data["system"].get( "flowSensorWateringClicks" ) @@ -174,4 +174,4 @@ class UniversalRestrictionsSensor(RainMachineSensor): def update_from_latest_data(self) -> None: """Update the state.""" if self._entity_type == TYPE_FREEZE_TEMP: - self._attr_state = self.coordinator.data["freezeProtectTemp"] + self._attr_native_value = self.coordinator.data["freezeProtectTemp"] diff --git a/homeassistant/components/random/sensor.py b/homeassistant/components/random/sensor.py index 6465b828be1..91a34639de1 100644 --- a/homeassistant/components/random/sensor.py +++ b/homeassistant/components/random/sensor.py @@ -58,7 +58,7 @@ class RandomSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state @@ -68,7 +68,7 @@ class RandomSensor(SensorEntity): return ICON @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index beb7c182351..304eaafb85f 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -127,4 +127,4 @@ class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): ATTR_NEXT_PICKUP_DATE: as_utc(next_pickup_event.date).isoformat(), } ) - self._attr_state = as_utc(pickup_event.date).isoformat() + self._attr_native_value = as_utc(pickup_event.date).isoformat() diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py index 7472ad42301..2e1ec5dc18a 100644 --- a/homeassistant/components/reddit/sensor.py +++ b/homeassistant/components/reddit/sensor.py @@ -99,7 +99,7 @@ class RedditSensor(SensorEntity): return f"reddit_{self._subreddit}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return len(self._subreddit_data) diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py index 78b713c286c..99e2f90c879 100644 --- a/homeassistant/components/rejseplanen/sensor.py +++ b/homeassistant/components/rejseplanen/sensor.py @@ -105,7 +105,7 @@ class RejseplanenTransportSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -131,7 +131,7 @@ class RejseplanenTransportSensor(SensorEntity): return attributes @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return TIME_MINUTES diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py index 46818095647..04cff82bcf3 100644 --- a/homeassistant/components/repetier/sensor.py +++ b/homeassistant/components/repetier/sensor.py @@ -77,7 +77,7 @@ class RepetierSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return SENSOR_TYPES[self._sensor_type][1] @@ -92,7 +92,7 @@ class RepetierSensor(SensorEntity): return False @property - def state(self): + def native_value(self): """Return sensor state.""" return self._state @@ -134,7 +134,7 @@ class RepetierTempSensor(RepetierSensor): """Represent a Repetier temp sensor.""" @property - def state(self): + def native_value(self): """Return sensor state.""" if self._state is None: return None @@ -156,7 +156,7 @@ class RepetierJobSensor(RepetierSensor): """Represent a Repetier job sensor.""" @property - def state(self): + def native_value(self): """Return sensor state.""" if self._state is None: return None diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 7727b5f09ab..f0355014986 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -115,12 +115,12 @@ class RestSensor(RestEntity, SensorEntity): self._json_attrs_path = json_attrs_path @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/rflink/sensor.py b/homeassistant/components/rflink/sensor.py index 497c9b8cee6..6b0c9efe157 100644 --- a/homeassistant/components/rflink/sensor.py +++ b/homeassistant/components/rflink/sensor.py @@ -152,12 +152,12 @@ class RflinkSensor(RflinkDevice, SensorEntity): self.handle_event_callback(self._initial_event) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return measurement unit.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return value.""" return self._state diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 8b9d5e5c389..49a8bbb974c 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -78,92 +78,92 @@ SENSOR_TYPES = ( key="Barameter", device_class=DEVICE_CLASS_PRESSURE, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=PRESSURE_HPA, + native_unit_of_measurement=PRESSURE_HPA, ), RfxtrxSensorEntityDescription( key="Battery numeric", device_class=DEVICE_CLASS_BATTERY, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, convert=_battery_convert, ), RfxtrxSensorEntityDescription( key="Current", device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), RfxtrxSensorEntityDescription( key="Current Ch. 1", device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), RfxtrxSensorEntityDescription( key="Current Ch. 2", device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), RfxtrxSensorEntityDescription( key="Current Ch. 3", device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), RfxtrxSensorEntityDescription( key="Energy usage", device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, ), RfxtrxSensorEntityDescription( key="Humidity", device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, ), RfxtrxSensorEntityDescription( key="Rssi numeric", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, convert=_rssi_convert, ), RfxtrxSensorEntityDescription( key="Temperature", device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, ), RfxtrxSensorEntityDescription( key="Temperature2", device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, ), RfxtrxSensorEntityDescription( key="Total usage", device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_MEASUREMENT, last_reset=dt.utc_from_timestamp(0), - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), RfxtrxSensorEntityDescription( key="Voltage", device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, ), RfxtrxSensorEntityDescription( key="Wind direction", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=DEGREE, + native_unit_of_measurement=DEGREE, ), RfxtrxSensorEntityDescription( key="Rain rate", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, + native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, ), RfxtrxSensorEntityDescription( key="Sound", @@ -175,34 +175,34 @@ SENSOR_TYPES = ( key="Count", state_class=STATE_CLASS_MEASUREMENT, last_reset=dt.utc_from_timestamp(0), - unit_of_measurement="count", + native_unit_of_measurement="count", ), RfxtrxSensorEntityDescription( key="Counter value", state_class=STATE_CLASS_MEASUREMENT, last_reset=dt.utc_from_timestamp(0), - unit_of_measurement="count", + native_unit_of_measurement="count", ), RfxtrxSensorEntityDescription( key="Chill", device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, ), RfxtrxSensorEntityDescription( key="Wind average speed", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=SPEED_METERS_PER_SECOND, + native_unit_of_measurement=SPEED_METERS_PER_SECOND, ), RfxtrxSensorEntityDescription( key="Wind gust", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=SPEED_METERS_PER_SECOND, + native_unit_of_measurement=SPEED_METERS_PER_SECOND, ), RfxtrxSensorEntityDescription( key="Rain total", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=LENGTH_MILLIMETERS, + native_unit_of_measurement=LENGTH_MILLIMETERS, ), RfxtrxSensorEntityDescription( key="Forecast", @@ -216,7 +216,7 @@ SENSOR_TYPES = ( RfxtrxSensorEntityDescription( key="UV", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=UV_INDEX, + native_unit_of_measurement=UV_INDEX, ), ) @@ -313,7 +313,7 @@ class RfxtrxSensor(RfxtrxEntity, SensorEntity): self._apply_event(get_rfx_object(event)) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if not self._event: return None diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 97fb8ec9d21..192ba03c010 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -56,7 +56,7 @@ class RingSensor(RingEntityMixin, SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._sensor_type == "volume": return self._device.volume @@ -84,7 +84,7 @@ class RingSensor(RingEntityMixin, SensorEntity): return self._icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the units of measurement.""" return SENSOR_TYPES.get(self._sensor_type)[2] @@ -120,7 +120,7 @@ class HealthDataRingSensor(RingSensor): return False @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._sensor_type == "wifi_signal_category": return self._device.wifi_signal_category @@ -172,7 +172,7 @@ class HistoryRingSensor(RingSensor): self.async_write_ha_state() @property - def state(self): + def native_value(self): """Return the state of the sensor.""" if self._latest_event is None: return None diff --git a/homeassistant/components/ripple/sensor.py b/homeassistant/components/ripple/sensor.py index f36e2c58ec8..2746f5789cd 100644 --- a/homeassistant/components/ripple/sensor.py +++ b/homeassistant/components/ripple/sensor.py @@ -46,12 +46,12 @@ class RippleSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return self._unit_of_measurement diff --git a/homeassistant/components/risco/sensor.py b/homeassistant/components/risco/sensor.py index b39655949b2..0068e8c0f04 100644 --- a/homeassistant/components/risco/sensor.py +++ b/homeassistant/components/risco/sensor.py @@ -87,7 +87,7 @@ class RiscoSensor(CoordinatorEntity, SensorEntity): self.async_write_ha_state() @property - def state(self): + def native_value(self): """Value of sensor.""" if self._event is None: return None diff --git a/homeassistant/components/rituals_perfume_genie/sensor.py b/homeassistant/components/rituals_perfume_genie/sensor.py index 7c957722384..c4b330d1ccf 100644 --- a/homeassistant/components/rituals_perfume_genie/sensor.py +++ b/homeassistant/components/rituals_perfume_genie/sensor.py @@ -59,7 +59,7 @@ class DiffuserPerfumeSensor(DiffuserEntity): return "mdi:tag-remove" @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the perfume sensor.""" return self._diffuser.perfume @@ -81,7 +81,7 @@ class DiffuserFillSensor(DiffuserEntity): return "mdi:beaker-question" @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the fill sensor.""" return self._diffuser.fill @@ -90,7 +90,7 @@ class DiffuserBatterySensor(DiffuserEntity): """Representation of a diffuser battery sensor.""" _attr_device_class = DEVICE_CLASS_BATTERY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__( self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator @@ -99,7 +99,7 @@ class DiffuserBatterySensor(DiffuserEntity): super().__init__(diffuser, coordinator, BATTERY_SUFFIX) @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the battery sensor.""" return self._diffuser.battery_percentage @@ -108,7 +108,7 @@ class DiffuserWifiSensor(DiffuserEntity): """Representation of a diffuser wifi sensor.""" _attr_device_class = DEVICE_CLASS_SIGNAL_STRENGTH - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__( self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator @@ -117,6 +117,6 @@ class DiffuserWifiSensor(DiffuserEntity): super().__init__(diffuser, coordinator, WIFI_SUFFIX) @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the wifi sensor.""" return self._diffuser.wifi_percentage diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index 9e4e7f3d588..bf2eab2d7b7 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -145,7 +145,7 @@ class RMVDepartureSensor(SensorEntity): return self._state is not None @property - def state(self): + def native_value(self): """Return the next departure time.""" return self._state @@ -171,7 +171,7 @@ class RMVDepartureSensor(SensorEntity): return self._icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return TIME_MINUTES diff --git a/homeassistant/components/roomba/sensor.py b/homeassistant/components/roomba/sensor.py index 4a99d9f71af..bc20b4397e2 100644 --- a/homeassistant/components/roomba/sensor.py +++ b/homeassistant/components/roomba/sensor.py @@ -36,7 +36,7 @@ class RoombaBattery(IRobotEntity, SensorEntity): return DEVICE_CLASS_BATTERY @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit_of_measurement of the device.""" return PERCENTAGE @@ -50,6 +50,6 @@ class RoombaBattery(IRobotEntity, SensorEntity): ) @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._battery_level diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index 35d8c0ae2c0..54e2c315a4e 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_state = pickup_date.isoformat() + self._attr_native_value = pickup_date.isoformat() class RovaData: diff --git a/homeassistant/components/rtorrent/sensor.py b/homeassistant/components/rtorrent/sensor.py index c750c7aa83c..5379cb2ce2e 100644 --- a/homeassistant/components/rtorrent/sensor.py +++ b/homeassistant/components/rtorrent/sensor.py @@ -94,7 +94,7 @@ class RTorrentSensor(SensorEntity): return f"{self.client_name} {self._name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -104,7 +104,7 @@ class RTorrentSensor(SensorEntity): return self._available @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index c0930f2c114..ffe57e608bf 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -45,7 +45,7 @@ class SabnzbdSensor(SensorEntity): return f"{self._client_name} {self._name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -55,7 +55,7 @@ class SabnzbdSensor(SensorEntity): return False @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 1b46632051e..795823b9e9f 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -191,12 +191,12 @@ class SAJsensor(SensorEntity): return f"saj_{self._sensor.name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return SAJ_UNIT_MAPPINGS[self._sensor.unit] diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 921ab29f714..1f5b543f9ee 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -108,12 +108,12 @@ class ScrapeSensor(SensorEntity): return self._name @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state diff --git a/homeassistant/components/screenlogic/sensor.py b/homeassistant/components/screenlogic/sensor.py index 1ad18298655..c8e4f84caf0 100644 --- a/homeassistant/components/screenlogic/sensor.py +++ b/homeassistant/components/screenlogic/sensor.py @@ -114,7 +114,7 @@ class ScreenLogicSensor(ScreenlogicEntity, SensorEntity): return f"{self.gateway_name} {self.sensor['name']}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self.sensor.get("unit") @@ -125,7 +125,7 @@ class ScreenLogicSensor(ScreenlogicEntity, SensorEntity): return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_type) @property - def state(self): + def native_value(self): """State of the sensor.""" value = self.sensor["value"] return (value - 1) if "supply" in self._data_key else value @@ -160,7 +160,7 @@ class ScreenLogicChemistrySensor(ScreenLogicSensor): self._key = key @property - def state(self): + def native_value(self): """State of the sensor.""" value = self.sensor["value"] if "dosing_state" in self._key: diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index 165920dd8e5..80fb71f594b 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -126,7 +126,7 @@ class Season(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the current season.""" return self.season diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index 5a352969c3b..69cae55ff31 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -125,7 +125,7 @@ class SenseActiveSensor(SensorEntity): """Implementation of a Sense energy sensor.""" _attr_icon = ICON - _attr_unit_of_measurement = POWER_WATT + _attr_native_unit_of_measurement = POWER_WATT _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_should_poll = False _attr_available = False @@ -168,9 +168,9 @@ class SenseActiveSensor(SensorEntity): if self._is_production else self._data.active_power ) - if self._attr_available and self._attr_state == new_state: + if self._attr_available and self._attr_native_value == new_state: return - self._attr_state = new_state + self._attr_native_value = new_state self._attr_available = True self.async_write_ha_state() @@ -178,7 +178,7 @@ class SenseActiveSensor(SensorEntity): class SenseVoltageSensor(SensorEntity): """Implementation of a Sense energy voltage sensor.""" - _attr_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT + _attr_native_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_icon = ICON _attr_should_poll = False @@ -212,10 +212,10 @@ class SenseVoltageSensor(SensorEntity): def _async_update_from_data(self): """Update the sensor from the data. Must not do I/O.""" new_state = round(self._data.active_voltage[self._voltage_index], 1) - if self._attr_available and self._attr_state == new_state: + if self._attr_available and self._attr_native_value == new_state: return self._attr_available = True - self._attr_state = new_state + self._attr_native_value = new_state self.async_write_ha_state() @@ -224,7 +224,7 @@ class SenseTrendsSensor(SensorEntity): _attr_device_class = DEVICE_CLASS_ENERGY _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR + _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_icon = ICON _attr_should_poll = False @@ -249,7 +249,7 @@ class SenseTrendsSensor(SensorEntity): self._had_any_update = False @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return round(self._data.get_trend(self._sensor_type, self._is_production), 1) @@ -288,7 +288,7 @@ class SenseEnergyDevice(SensorEntity): _attr_available = False _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_unit_of_measurement = POWER_WATT + _attr_native_unit_of_measurement = POWER_WATT _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_device_class = DEVICE_CLASS_POWER _attr_should_poll = False @@ -320,8 +320,8 @@ class SenseEnergyDevice(SensorEntity): new_state = 0 else: new_state = int(device_data["w"]) - if self._attr_available and self._attr_state == new_state: + if self._attr_available and self._attr_native_value == new_state: return - self._attr_state = new_state + self._attr_native_value = new_state self._attr_available = True self.async_write_ha_state() diff --git a/homeassistant/components/sensehat/sensor.py b/homeassistant/components/sensehat/sensor.py index 379301b0fa7..9274f133441 100644 --- a/homeassistant/components/sensehat/sensor.py +++ b/homeassistant/components/sensehat/sensor.py @@ -86,12 +86,12 @@ class SenseHatSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/serial/sensor.py b/homeassistant/components/serial/sensor.py index 1e73ae9ac83..cbdba50b6b6 100644 --- a/homeassistant/components/serial/sensor.py +++ b/homeassistant/components/serial/sensor.py @@ -245,6 +245,6 @@ class SerialSensor(SensorEntity): return self._attributes @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/serial_pm/sensor.py b/homeassistant/components/serial_pm/sensor.py index fd017661de2..9332f268308 100644 --- a/homeassistant/components/serial_pm/sensor.py +++ b/homeassistant/components/serial_pm/sensor.py @@ -71,12 +71,12 @@ class ParticulateMatterSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return CONCENTRATION_MICROGRAMS_PER_CUBIC_METER diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index ab0f0779656..44720db2fcb 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -125,7 +125,7 @@ class SeventeenTrackSummarySensor(SensorEntity): return f"Seventeentrack Packages {self._status}" @property - def state(self): + def native_value(self): """Return the state.""" return self._state @@ -135,7 +135,7 @@ class SeventeenTrackSummarySensor(SensorEntity): return f"summary_{self._data.account_id}_{slugify(self._status)}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return "packages" @@ -211,7 +211,7 @@ class SeventeenTrackPackageSensor(SensorEntity): return f"Seventeentrack Package: {name}" @property - def state(self): + def native_value(self): """Return the state.""" return self._state diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 07e4f4a4fe3..e3af10571d5 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -280,7 +280,7 @@ class ShellySensor(ShellyBlockAttributeEntity, SensorEntity): ).replace(second=0, microsecond=0) @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return value of sensor.""" if ( self.description.last_reset == LAST_RESET_UPTIME @@ -302,7 +302,7 @@ class ShellySensor(ShellyBlockAttributeEntity, SensorEntity): return self.description.state_class @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return unit of sensor.""" return cast(str, self._unit) @@ -311,7 +311,7 @@ class ShellyRestSensor(ShellyRestAttributeEntity, SensorEntity): """Represent a shelly REST sensor.""" @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return value of sensor.""" return self.attribute_value @@ -321,7 +321,7 @@ class ShellyRestSensor(ShellyRestAttributeEntity, SensorEntity): return self.description.state_class @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return unit of sensor.""" return self.description.unit @@ -330,7 +330,7 @@ class ShellySleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity): """Represent a shelly sleeping sensor.""" @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return value of sensor.""" if self.block is not None: return self.attribute_value @@ -343,6 +343,6 @@ class ShellySleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity): return self.description.state_class @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return unit of sensor.""" return cast(str, self._unit) diff --git a/homeassistant/components/shodan/sensor.py b/homeassistant/components/shodan/sensor.py index fa0fc2d3906..1423a3b9327 100644 --- a/homeassistant/components/shodan/sensor.py +++ b/homeassistant/components/shodan/sensor.py @@ -62,12 +62,12 @@ class ShodanSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/sht31/sensor.py b/homeassistant/components/sht31/sensor.py index a894623db47..1b1e1427e51 100644 --- a/homeassistant/components/sht31/sensor.py +++ b/homeassistant/components/sht31/sensor.py @@ -108,7 +108,7 @@ class SHTSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -123,7 +123,7 @@ class SHTSensorTemperature(SHTSensor): _attr_device_class = DEVICE_CLASS_TEMPERATURE @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self.hass.config.units.temperature_unit @@ -141,7 +141,7 @@ class SHTSensorHumidity(SHTSensor): """Representation of a humidity sensor.""" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return PERCENTAGE diff --git a/homeassistant/components/sigfox/sensor.py b/homeassistant/components/sigfox/sensor.py index 75c2a4f0f63..41fb3469293 100644 --- a/homeassistant/components/sigfox/sensor.py +++ b/homeassistant/components/sigfox/sensor.py @@ -149,7 +149,7 @@ class SigfoxDevice(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the payload of the last message.""" return self._state diff --git a/homeassistant/components/simplisafe/sensor.py b/homeassistant/components/simplisafe/sensor.py index 149319cd5bd..c3f8d7c3ab0 100644 --- a/homeassistant/components/simplisafe/sensor.py +++ b/homeassistant/components/simplisafe/sensor.py @@ -34,9 +34,9 @@ class SimplisafeFreezeSensor(SimpliSafeBaseSensor, SensorEntity): """Define a SimpliSafe freeze sensor entity.""" _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_unit_of_measurement = TEMP_FAHRENHEIT + _attr_native_unit_of_measurement = TEMP_FAHRENHEIT @callback def async_update_from_rest_api(self) -> None: """Update the entity with the provided REST API data.""" - self._attr_state = self._sensor.temperature + self._attr_native_value = self._sensor.temperature diff --git a/homeassistant/components/simulated/sensor.py b/homeassistant/components/simulated/sensor.py index 3fe7aedfbb0..819f9c7147c 100644 --- a/homeassistant/components/simulated/sensor.py +++ b/homeassistant/components/simulated/sensor.py @@ -121,7 +121,7 @@ class SimulatedSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -131,7 +131,7 @@ class SimulatedSensor(SensorEntity): return ICON @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return self._unit diff --git a/homeassistant/components/skybeacon/sensor.py b/homeassistant/components/skybeacon/sensor.py index 5b6eae96a7e..a72e1372ca0 100644 --- a/homeassistant/components/skybeacon/sensor.py +++ b/homeassistant/components/skybeacon/sensor.py @@ -65,7 +65,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class SkybeaconHumid(SensorEntity): """Representation of a Skybeacon humidity sensor.""" - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, name, mon): """Initialize a sensor.""" @@ -78,7 +78,7 @@ class SkybeaconHumid(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the device.""" return self.mon.data["humid"] @@ -92,7 +92,7 @@ class SkybeaconTemp(SensorEntity): """Representation of a Skybeacon temperature sensor.""" _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_unit_of_measurement = TEMP_CELSIUS + _attr_native_unit_of_measurement = TEMP_CELSIUS def __init__(self, name, mon): """Initialize a sensor.""" @@ -105,7 +105,7 @@ class SkybeaconTemp(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the device.""" return self.mon.data["temp"] diff --git a/homeassistant/components/skybell/sensor.py b/homeassistant/components/skybell/sensor.py index cee864911b4..0ac26c1c76b 100644 --- a/homeassistant/components/skybell/sensor.py +++ b/homeassistant/components/skybell/sensor.py @@ -71,4 +71,4 @@ class SkybellSensor(SkybellDevice, SensorEntity): super().update() if self.entity_description.key == "chime_level": - self._attr_state = self._device.outdoor_chime_level + self._attr_native_value = self._device.outdoor_chime_level diff --git a/homeassistant/components/sleepiq/sensor.py b/homeassistant/components/sleepiq/sensor.py index 8f5c17dad89..eec096e56c2 100644 --- a/homeassistant/components/sleepiq/sensor.py +++ b/homeassistant/components/sleepiq/sensor.py @@ -37,7 +37,7 @@ class SleepNumberSensor(SleepIQSensor, SensorEntity): self.update() @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 6f3f7c2dca9..36084c53bb3 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -179,12 +179,12 @@ class SMAsensor(CoordinatorEntity, SensorEntity): return self._sensor.name @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the state of the sensor.""" return self._sensor.value @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" return self._sensor.unit diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index c7f30a8b954..fb879e3cef5 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -282,7 +282,7 @@ class SmappeeSensor(SensorEntity): return self._icon @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @@ -292,7 +292,7 @@ class SmappeeSensor(SensorEntity): return self._device_class @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/homeassistant/components/smart_meter_texas/sensor.py b/homeassistant/components/smart_meter_texas/sensor.py index f63edcce0fc..6914d3ef1ac 100644 --- a/homeassistant/components/smart_meter_texas/sensor.py +++ b/homeassistant/components/smart_meter_texas/sensor.py @@ -33,7 +33,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SmartMeterTexasSensor(CoordinatorEntity, RestoreEntity, SensorEntity): """Representation of an Smart Meter Texas sensor.""" - _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR + _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR def __init__(self, meter: Meter, coordinator: DataUpdateCoordinator) -> None: """Initialize the sensor.""" @@ -58,7 +58,7 @@ class SmartMeterTexasSensor(CoordinatorEntity, RestoreEntity, SensorEntity): return self._available @property - def state(self): + def native_value(self): """Get the latest reading.""" return self._state diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index cb8fa4bb6d2..c4d84ec5f69 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -492,7 +492,7 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): return f"{self._device.device_id}.{self._attribute}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._device.status.attributes[self._attribute].value @@ -502,7 +502,7 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): return self._device_class @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" unit = self._device.status.attributes[self._attribute].unit return UNITS.get(unit, unit) if unit else self._default_unit @@ -534,7 +534,7 @@ class SmartThingsThreeAxisSensor(SmartThingsEntity, SensorEntity): return f"{self._device.device_id}.{THREE_AXIS_NAMES[self._index]}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" three_axis = self._device.status.attributes[Attribute.three_axis].value try: diff --git a/homeassistant/components/smarttub/sensor.py b/homeassistant/components/smarttub/sensor.py index 95a862502cd..9922792ba12 100644 --- a/homeassistant/components/smarttub/sensor.py +++ b/homeassistant/components/smarttub/sensor.py @@ -87,7 +87,7 @@ class SmartTubSensor(SmartTubSensorBase, SensorEntity): """Generic class for SmartTub status sensors.""" @property - def state(self) -> str: + def native_value(self) -> str: """Return the current state of the sensor.""" if isinstance(self._state, Enum): return self._state.name.lower() @@ -109,7 +109,7 @@ class SmartTubPrimaryFiltrationCycle(SmartTubSensor): return self._state @property - def state(self) -> str: + def native_value(self) -> str: """Return the current state of the sensor.""" return self.cycle.status.name.lower() @@ -147,7 +147,7 @@ class SmartTubSecondaryFiltrationCycle(SmartTubSensor): return self._state @property - def state(self) -> str: + def native_value(self) -> str: """Return the current state of the sensor.""" return self.cycle.status.name.lower() diff --git a/homeassistant/components/smarty/sensor.py b/homeassistant/components/smarty/sensor.py index b958185f9bd..a76e4b0f567 100644 --- a/homeassistant/components/smarty/sensor.py +++ b/homeassistant/components/smarty/sensor.py @@ -64,12 +64,12 @@ class SmartySensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/sms/sensor.py b/homeassistant/components/sms/sensor.py index eb6c6ab22e1..d405c817656 100644 --- a/homeassistant/components/sms/sensor.py +++ b/homeassistant/components/sms/sensor.py @@ -25,7 +25,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): key="signal", name=f"gsm_signal_imei_{imei}", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, - unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, entity_registry_enabled_default=False, ), ) @@ -55,7 +55,7 @@ class GSMSignalSensor(SensorEntity): return self._state is not None @property - def state(self): + def native_value(self): """Return the state of the device.""" return self._state["SignalStrength"] diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index 7de2bfb91e2..09bfe3856cc 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -155,12 +155,12 @@ class SnmpSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/sochain/sensor.py b/homeassistant/components/sochain/sensor.py index 1f735da4995..a4cdd595f90 100644 --- a/homeassistant/components/sochain/sensor.py +++ b/homeassistant/components/sochain/sensor.py @@ -54,7 +54,7 @@ class SochainSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return ( self.chainso.data.get("confirmed_balance") @@ -63,7 +63,7 @@ class SochainSensor(SensorEntity): ) @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return self._unit_of_measurement diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py index 872781bf19c..06d8813130e 100644 --- a/homeassistant/components/solaredge/const.py +++ b/homeassistant/components/solaredge/const.py @@ -42,7 +42,7 @@ SENSOR_TYPES = [ icon="mdi:solar-power", last_reset=dt_util.utc_from_timestamp(0), state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=ENERGY_WATT_HOUR, + native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), SolarEdgeSensorEntityDescription( @@ -51,7 +51,7 @@ SENSOR_TYPES = [ name="Energy this year", entity_registry_enabled_default=False, icon="mdi:solar-power", - unit_of_measurement=ENERGY_WATT_HOUR, + native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), SolarEdgeSensorEntityDescription( @@ -60,7 +60,7 @@ SENSOR_TYPES = [ name="Energy this month", entity_registry_enabled_default=False, icon="mdi:solar-power", - unit_of_measurement=ENERGY_WATT_HOUR, + native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), SolarEdgeSensorEntityDescription( @@ -69,7 +69,7 @@ SENSOR_TYPES = [ name="Energy today", entity_registry_enabled_default=False, icon="mdi:solar-power", - unit_of_measurement=ENERGY_WATT_HOUR, + native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), SolarEdgeSensorEntityDescription( @@ -78,7 +78,7 @@ SENSOR_TYPES = [ name="Current Power", icon="mdi:solar-power", state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, ), SolarEdgeSensorEntityDescription( @@ -185,6 +185,6 @@ SENSOR_TYPES = [ name="Storage Level", entity_registry_enabled_default=False, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, ), ] diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 85e01a2d7ee..23aa269cf36 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -133,7 +133,7 @@ class SolarEdgeOverviewSensor(SolarEdgeSensorEntity): """Representation of an SolarEdge Monitoring API overview sensor.""" @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" return self.data_service.data.get(self.entity_description.json_key) @@ -147,7 +147,7 @@ class SolarEdgeDetailsSensor(SolarEdgeSensorEntity): return self.data_service.attributes @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" return self.data_service.data @@ -161,7 +161,7 @@ class SolarEdgeInventorySensor(SolarEdgeSensorEntity): return self.data_service.attributes.get(self.entity_description.json_key) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" return self.data_service.data.get(self.entity_description.json_key) @@ -173,7 +173,7 @@ class SolarEdgeEnergyDetailsSensor(SolarEdgeSensorEntity): """Initialize the power flow sensor.""" super().__init__(platform_name, sensor_type, data_service) - self._attr_unit_of_measurement = data_service.unit + self._attr_native_unit_of_measurement = data_service.unit @property def extra_state_attributes(self) -> dict[str, Any]: @@ -181,7 +181,7 @@ class SolarEdgeEnergyDetailsSensor(SolarEdgeSensorEntity): return self.data_service.attributes.get(self.entity_description.json_key) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" return self.data_service.data.get(self.entity_description.json_key) @@ -200,7 +200,7 @@ class SolarEdgePowerFlowSensor(SolarEdgeSensorEntity): """Initialize the power flow sensor.""" super().__init__(platform_name, description, data_service) - self._attr_unit_of_measurement = data_service.unit + self._attr_native_unit_of_measurement = data_service.unit @property def extra_state_attributes(self) -> dict[str, Any]: @@ -208,7 +208,7 @@ class SolarEdgePowerFlowSensor(SolarEdgeSensorEntity): return self.data_service.attributes.get(self.entity_description.json_key) @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" return self.data_service.data.get(self.entity_description.json_key) @@ -219,7 +219,7 @@ class SolarEdgeStorageLevelSensor(SolarEdgeSensorEntity): _attr_device_class = DEVICE_CLASS_BATTERY @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of the sensor.""" attr = self.data_service.attributes.get(self.entity_description.json_key) if attr and "soc" in attr: diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index 3f159ce4480..f9ac2b853e7 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -285,7 +285,7 @@ class SolarEdgeSensor(SensorEntity): return f"{self._platform_name} ({self._name})" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement @@ -305,7 +305,7 @@ class SolarEdgeSensor(SensorEntity): return self._icon @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index 85a1531090d..a6a35bad80c 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -82,7 +82,7 @@ class SolarlogSensor(SensorEntity): return f"{self.device_name} {self._label}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the state of the sensor.""" return self._unit_of_measurement @@ -92,7 +92,7 @@ class SolarlogSensor(SensorEntity): return self._icon @property - def state(self): + def native_value(self): """Return the state of the sensor.""" return self._state diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index e47f5c57802..4d1652e8b12 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -84,7 +84,7 @@ class Inverter(SensorEntity): self.unit = unit @property - def state(self): + def native_value(self): """State of this inverter attribute.""" return self.value @@ -99,7 +99,7 @@ class Inverter(SensorEntity): return f"Solax {self.serial} {self.key}" @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self.unit diff --git a/homeassistant/components/soma/sensor.py b/homeassistant/components/soma/sensor.py index 4df12c9f8f5..948e8d1e1e1 100644 --- a/homeassistant/components/soma/sensor.py +++ b/homeassistant/components/soma/sensor.py @@ -30,7 +30,7 @@ class SomaSensor(SomaEntity, SensorEntity): """Representation of a Soma cover device.""" _attr_device_class = DEVICE_CLASS_BATTERY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE @property def name(self): @@ -38,7 +38,7 @@ class SomaSensor(SomaEntity, SensorEntity): return self.device["name"] + " battery level" @property - def state(self): + def native_value(self): """Return the state of the entity.""" return self.battery_state diff --git a/homeassistant/components/somfy/sensor.py b/homeassistant/components/somfy/sensor.py index 1817ba3fd8c..9a0602cb592 100644 --- a/homeassistant/components/somfy/sensor.py +++ b/homeassistant/components/somfy/sensor.py @@ -30,7 +30,7 @@ class SomfyThermostatBatterySensor(SomfyEntity, SensorEntity): """Representation of a Somfy thermostat battery.""" _attr_device_class = DEVICE_CLASS_BATTERY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, coordinator, device_id): """Initialize the Somfy device.""" @@ -43,6 +43,6 @@ class SomfyThermostatBatterySensor(SomfyEntity, SensorEntity): self._climate = Thermostat(self.device, self.coordinator.client) @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" return self._climate.get_battery() diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py index d173d42eaf7..3f5ef275fef 100644 --- a/homeassistant/components/sonarr/sensor.py +++ b/homeassistant/components/sonarr/sensor.py @@ -85,7 +85,7 @@ class SonarrSensor(SonarrEntity, SensorEntity): self._attr_name = name self._attr_icon = icon self._attr_unique_id = f"{entry_id}_{key}" - self._attr_unit_of_measurement = unit_of_measurement + self._attr_native_unit_of_measurement = unit_of_measurement self._attr_entity_registry_enabled_default = enabled_default self.last_update_success = False @@ -134,7 +134,7 @@ class SonarrCommandsSensor(SonarrSensor): return attrs @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" return len(self._commands) @@ -181,7 +181,7 @@ class SonarrDiskspaceSensor(SonarrSensor): return attrs @property - def state(self) -> str: + def native_value(self) -> str: """Return the state of the sensor.""" free = self._total_free / 1024 ** 3 return f"{free:.2f}" @@ -223,7 +223,7 @@ class SonarrQueueSensor(SonarrSensor): return attrs @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" return len(self._queue) @@ -261,7 +261,7 @@ class SonarrSeriesSensor(SonarrSensor): return attrs @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" return len(self._items) @@ -304,7 +304,7 @@ class SonarrUpcomingSensor(SonarrSensor): return attrs @property - def state(self) -> int: + def native_value(self) -> int: """Return the state of the sensor.""" return len(self._upcoming) @@ -347,6 +347,6 @@ class SonarrWantedSensor(SonarrSensor): return attrs @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of the sensor.""" return self._total diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index 9e5277819a7..1a13e6f55f4 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -45,7 +45,7 @@ class SonosBatteryEntity(SonosEntity, SensorEntity): return DEVICE_CLASS_BATTERY @property - def unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str: """Get the unit of measurement.""" return PERCENTAGE @@ -54,7 +54,7 @@ class SonosBatteryEntity(SonosEntity, SensorEntity): await self.speaker.async_poll_battery() @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of the sensor.""" return self.speaker.battery_info.get("Level") diff --git a/homeassistant/components/speedtestdotnet/const.py b/homeassistant/components/speedtestdotnet/const.py index 897ffa126fa..c9962362406 100644 --- a/homeassistant/components/speedtestdotnet/const.py +++ b/homeassistant/components/speedtestdotnet/const.py @@ -17,19 +17,19 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( SensorEntityDescription( key="ping", name="Ping", - unit_of_measurement=TIME_MILLISECONDS, + native_unit_of_measurement=TIME_MILLISECONDS, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="download", name="Download", - unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, + native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="upload", name="Upload", - unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, + native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, state_class=STATE_CLASS_MEASUREMENT, ), ) diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index 1c6c80a6af1..2dc12c956de 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -85,7 +85,7 @@ class SpeedtestSensor(CoordinatorEntity, RestoreEntity, SensorEntity): await super().async_added_to_hass() state = await self.async_get_last_state() if state: - self._attr_state = state.state + self._attr_native_value = state.state @callback def update() -> None: @@ -100,8 +100,12 @@ class SpeedtestSensor(CoordinatorEntity, RestoreEntity, SensorEntity): """Update sensors state.""" if self.coordinator.data: if self.entity_description.key == "ping": - self._attr_state = self.coordinator.data["ping"] + self._attr_native_value = self.coordinator.data["ping"] elif self.entity_description.key == "download": - self._attr_state = round(self.coordinator.data["download"] / 10 ** 6, 2) + self._attr_native_value = round( + self.coordinator.data["download"] / 10 ** 6, 2 + ) elif self.entity_description.key == "upload": - self._attr_state = round(self.coordinator.data["upload"] / 10 ** 6, 2) + self._attr_native_value = round( + self.coordinator.data["upload"] / 10 ** 6, 2 + ) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 4c1c29b82a6..1b0ae5a9076 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -123,12 +123,12 @@ class SQLSensor(SensorEntity): return self._name @property - def state(self): + def native_value(self): """Return the query's current state.""" return self._state @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py index 6973c58600e..97b65840e83 100644 --- a/homeassistant/components/srp_energy/sensor.py +++ b/homeassistant/components/srp_energy/sensor.py @@ -93,14 +93,14 @@ class SrpEntity(SensorEntity): return self.type @property - def state(self): + def native_value(self): """Return the state of the device.""" if self._state: return f"{self._state:.2f}" return None @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement diff --git a/tests/components/sleepiq/test_sensor.py b/tests/components/sleepiq/test_sensor.py index c6a584802b9..7a7e47f03fa 100644 --- a/tests/components/sleepiq/test_sensor.py +++ b/tests/components/sleepiq/test_sensor.py @@ -21,15 +21,17 @@ async def test_setup(hass, requests_mock): assert len(devices) == 2 left_side = devices[1] + left_side.hass = hass assert left_side.name == "SleepNumber ILE Test1 SleepNumber" assert left_side.state == 40 right_side = devices[0] + right_side.hass = hass assert right_side.name == "SleepNumber ILE Test2 SleepNumber" assert right_side.state == 80 -async def test_setup_sigle(hass, requests_mock): +async def test_setup_single(hass, requests_mock): """Test for successfully setting up the SleepIQ platform.""" mock_responses(requests_mock, single=True) @@ -41,5 +43,6 @@ async def test_setup_sigle(hass, requests_mock): assert len(devices) == 1 right_side = devices[0] + right_side.hass = hass assert right_side.name == "SleepNumber ILE Test1 SleepNumber" assert right_side.state == 40 diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py index 069dc9eb64f..3e830b7fc93 100644 --- a/tests/components/srp_energy/test_sensor.py +++ b/tests/components/srp_energy/test_sensor.py @@ -82,6 +82,7 @@ async def test_srp_entity(hass): """Test the SrpEntity.""" fake_coordinator = MagicMock(data=1.99999999999) srp_entity = SrpEntity(fake_coordinator) + srp_entity.hass = hass assert srp_entity is not None assert srp_entity.name == f"{DEFAULT_NAME} {SENSOR_NAME}" @@ -104,6 +105,7 @@ async def test_srp_entity_no_data(hass): """Test the SrpEntity.""" fake_coordinator = MagicMock(data=False) srp_entity = SrpEntity(fake_coordinator) + srp_entity.hass = hass assert srp_entity.extra_state_attributes is None @@ -111,6 +113,7 @@ async def test_srp_entity_no_coord_data(hass): """Test the SrpEntity.""" fake_coordinator = MagicMock(data=False) srp_entity = SrpEntity(fake_coordinator) + srp_entity.hass = hass assert srp_entity.usage is None @@ -124,6 +127,7 @@ async def test_srp_entity_async_update(hass): MagicMock.__await__ = lambda x: async_magic().__await__() fake_coordinator = MagicMock(data=False) srp_entity = SrpEntity(fake_coordinator) + srp_entity.hass = hass await srp_entity.async_update() assert fake_coordinator.async_request_refresh.called From 87e0b1428283bfdd32f6988a24916dd5472e08f3 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 12 Aug 2021 11:46:07 -0500 Subject: [PATCH 176/355] Log gathered exceptions during Sonos unsubscriptions (#54190) --- homeassistant/components/sonos/__init__.py | 3 +-- homeassistant/components/sonos/speaker.py | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 45f5cf9276c..ae3652683d4 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -187,8 +187,7 @@ class SonosDiscoveryManager: async def _async_stop_event_listener(self, event: Event | None = None) -> None: await asyncio.gather( - *(speaker.async_unsubscribe() for speaker in self.data.discovered.values()), - return_exceptions=True, + *(speaker.async_unsubscribe() for speaker in self.data.discovered.values()) ) if events_asyncio.event_listener: await events_asyncio.event_listener.async_stop() diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 919e03cf39b..18f05d7341c 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -346,10 +346,13 @@ class SonosSpeaker: async def async_unsubscribe(self) -> None: """Cancel all subscriptions.""" _LOGGER.debug("Unsubscribing from events for %s", self.zone_name) - await asyncio.gather( + results = await asyncio.gather( *(subscription.unsubscribe() for subscription in self._subscriptions), return_exceptions=True, ) + for result in results: + if isinstance(result, Exception): + _LOGGER.debug("Unsubscribe failed for %s: %s", self.zone_name, result) self._subscriptions = [] @callback From 81e1c4459228361cd335789527437f0a1939f873 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 12 Aug 2021 12:18:10 -0700 Subject: [PATCH 177/355] Remove unused import step in OpenUV config flow (#54554) --- homeassistant/components/openuv/config_flow.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index 3595b124053..d8652ae09c5 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -51,10 +51,6 @@ class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors if errors else {}, ) - 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_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: From 084737dd01bc6405700d2c557dfce3e60b121c24 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 12 Aug 2021 15:01:34 -0500 Subject: [PATCH 178/355] Cleanup Sonos grouping event callback method (#54542) --- homeassistant/components/sonos/speaker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 18f05d7341c..6e887dfdc53 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -636,8 +636,8 @@ class SonosSpeaker: def async_update_groups(self, event: SonosEvent) -> None: """Handle callback for topology change event.""" if not hasattr(event, "zone_player_uui_ds_in_group"): - return None - self.hass.async_add_job(self.create_update_groups_coro(event)) + return + self.hass.async_create_task(self.create_update_groups_coro(event)) def create_update_groups_coro(self, event: SonosEvent | None = None) -> Coroutine: """Handle callback for topology change event.""" From 3f80c31bd51d41fb1b31eace41bc7e0d7fce6134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 12 Aug 2021 23:40:42 +0300 Subject: [PATCH 179/355] Remove obsolete upcloud YAML config support (#54516) --- homeassistant/components/upcloud/__init__.py | 73 ++----------------- .../components/upcloud/config_flow.py | 7 -- 2 files changed, 5 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index 9b76c209403..82d42e28589 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -8,11 +8,10 @@ from typing import Any, Dict import requests.exceptions import upcloud_api -import voluptuous as vol 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 SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_PASSWORD, CONF_SCAN_INTERVAL, @@ -23,18 +22,16 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -from .const import CONFIG_ENTRY_UPDATE_SIGNAL_TEMPLATE, DEFAULT_SCAN_INTERVAL, DOMAIN +from .const import CONFIG_ENTRY_UPDATE_SIGNAL_TEMPLATE, DEFAULT_SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) @@ -58,21 +55,6 @@ SIGNAL_UPDATE_UPCLOUD = "upcloud_update" STATE_MAP = {"error": STATE_PROBLEM, "started": STATE_ON, "stopped": STATE_OFF} -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - class UpCloudDataUpdateCoordinator( DataUpdateCoordinator[Dict[str, upcloud_api.Server]] @@ -115,37 +97,6 @@ class UpCloudHassData: coordinators: dict[str, UpCloudDataUpdateCoordinator] = dataclasses.field( default_factory=dict ) - scan_interval_migrations: dict[str, int] = dataclasses.field(default_factory=dict) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up UpCloud component.""" - domain_config = config.get(DOMAIN) - if not domain_config: - return True - - _LOGGER.warning( - "Loading upcloud via top level config is deprecated and no longer " - "necessary as of 0.117; Please remove it from your YAML configuration" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_USERNAME: domain_config[CONF_USERNAME], - CONF_PASSWORD: domain_config[CONF_PASSWORD], - }, - ) - ) - - if domain_config[CONF_SCAN_INTERVAL]: - hass.data[DATA_UPCLOUD] = UpCloudHassData() - hass.data[DATA_UPCLOUD].scan_interval_migrations[ - domain_config[CONF_USERNAME] - ] = domain_config[CONF_SCAN_INTERVAL] - - return True def _config_entry_update_signal_name(config_entry: ConfigEntry) -> str: @@ -178,22 +129,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Failed to connect", exc_info=True) raise ConfigEntryNotReady from err - upcloud_data = hass.data.setdefault(DATA_UPCLOUD, UpCloudHassData()) - - # Handle pre config entry (0.117) scan interval migration to options - migrated_scan_interval = upcloud_data.scan_interval_migrations.pop( - entry.data[CONF_USERNAME], None - ) - if migrated_scan_interval and ( - not entry.options.get(CONF_SCAN_INTERVAL) - or entry.options[CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL.total_seconds() - ): - update_interval = migrated_scan_interval - hass.config_entries.async_update_entry( - entry, - options={CONF_SCAN_INTERVAL: update_interval.total_seconds()}, - ) - elif entry.options.get(CONF_SCAN_INTERVAL): + if entry.options.get(CONF_SCAN_INTERVAL): update_interval = timedelta(seconds=entry.options[CONF_SCAN_INTERVAL]) else: update_interval = DEFAULT_SCAN_INTERVAL @@ -218,7 +154,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ) - upcloud_data.coordinators[entry.data[CONF_USERNAME]] = coordinator + hass.data[DATA_UPCLOUD] = UpCloudHassData() + hass.data[DATA_UPCLOUD].coordinators[entry.data[CONF_USERNAME]] = coordinator # Forward entry setup hass.config_entries.async_setup_platforms(entry, CONFIG_ENTRY_DOMAINS) diff --git a/homeassistant/components/upcloud/config_flow.py b/homeassistant/components/upcloud/config_flow.py index 1a16a78cfa1..e6868be29b9 100644 --- a/homeassistant/components/upcloud/config_flow.py +++ b/homeassistant/components/upcloud/config_flow.py @@ -57,13 +57,6 @@ class UpCloudConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=user_input[CONF_USERNAME], data=user_input) - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: - """Handle import initiated flow.""" - await self.async_set_unique_id(user_input[CONF_USERNAME]) - self._abort_if_unique_id_configured() - - return await self.async_step_user(user_input=user_input) - @callback def _async_show_form( self, From b1fb8de0f5905e08dcb0ec8d439718b43a4dccdd Mon Sep 17 00:00:00 2001 From: carstenschroeder Date: Thu, 12 Aug 2021 22:40:56 +0200 Subject: [PATCH 180/355] Add state_class attribute to keba integration (#54271) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen --- homeassistant/components/keba/sensor.py | 126 +++++++++++------------- 1 file changed, 58 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/keba/sensor.py b/homeassistant/components/keba/sensor.py index 37b42cb3cbe..2c0108ca1cd 100644 --- a/homeassistant/components/keba/sensor.py +++ b/homeassistant/components/keba/sensor.py @@ -1,10 +1,18 @@ """Support for KEBA charging station sensors.""" -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ( +from homeassistant.components.sensor import ( + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, + POWER_KILO_WATT, ) +from homeassistant.util import dt from . import DOMAIN @@ -19,44 +27,56 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= sensors = [ KebaSensor( keba, - "Curr user", - "Max Current", "max_current", - "mdi:flash", - ELECTRIC_CURRENT_AMPERE, + SensorEntityDescription( + key="Curr user", + name="Max Current", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + ), ), KebaSensor( keba, - "Setenergy", - "Energy Target", "energy_target", - "mdi:gauge", - ENERGY_KILO_WATT_HOUR, + SensorEntityDescription( + key="Setenergy", + name="Energy Target", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), ), KebaSensor( keba, - "P", - "Charging Power", "charging_power", - "mdi:flash", - "kW", - DEVICE_CLASS_POWER, + SensorEntityDescription( + key="P", + name="Charging Power", + native_unit_of_measurement=POWER_KILO_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), ), KebaSensor( keba, - "E pres", - "Session Energy", "session_energy", - "mdi:gauge", - ENERGY_KILO_WATT_HOUR, + SensorEntityDescription( + key="E pres", + name="Session Energy", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + ), ), KebaSensor( keba, - "E total", - "Total Energy", "total_energy", - "mdi:gauge", - ENERGY_KILO_WATT_HOUR, + SensorEntityDescription( + key="E total", + name="Total Energy", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt.utc_from_timestamp(0), + ), ), ] async_add_entities(sensors) @@ -65,53 +85,23 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class KebaSensor(SensorEntity): """The entity class for KEBA charging stations sensors.""" - def __init__(self, keba, key, name, entity_type, icon, unit, device_class=None): + _attr_should_poll = False + + def __init__( + self, + keba, + entity_type, + description: SensorEntityDescription, + ): """Initialize the KEBA Sensor.""" self._keba = keba - self._key = key - self._name = name + self.entity_description = description self._entity_type = entity_type - self._icon = icon - self._unit = unit - self._device_class = device_class - self._state = None - self._attributes = {} + self._attr_name = f"{keba.device_name} {description.name}" + self._attr_unique_id = f"{keba.device_id}_{entity_type}" - @property - def should_poll(self): - """Deactivate polling. Data updated by KebaHandler.""" - return False - - @property - def unique_id(self): - """Return the unique ID of the binary sensor.""" - return f"{self._keba.device_id}_{self._entity_type}" - - @property - def name(self): - """Return the name of the device.""" - return f"{self._keba.device_name} {self._name}" - - @property - def device_class(self): - """Return the class of this sensor.""" - return self._device_class - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self._icon - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def native_unit_of_measurement(self): - """Get the unit of measurement.""" - return self._unit + self._attributes: dict[str, str] = {} @property def extra_state_attributes(self): @@ -120,9 +110,9 @@ class KebaSensor(SensorEntity): async def async_update(self): """Get latest cached states from the device.""" - self._state = self._keba.get_value(self._key) + self._attr_native_value = self._keba.get_value(self.entity_description.key) - if self._key == "P": + if self.entity_description.key == "P": self._attributes["power_factor"] = self._keba.get_value("PF") self._attributes["voltage_u1"] = str(self._keba.get_value("U1")) self._attributes["voltage_u2"] = str(self._keba.get_value("U2")) @@ -130,7 +120,7 @@ class KebaSensor(SensorEntity): self._attributes["current_i1"] = str(self._keba.get_value("I1")) self._attributes["current_i2"] = str(self._keba.get_value("I2")) self._attributes["current_i3"] = str(self._keba.get_value("I3")) - elif self._key == "Curr user": + elif self.entity_description.key == "Curr user": self._attributes["max_current_hardware"] = self._keba.get_value("Curr HW") def update_callback(self): From 84f568abb17ff4ff9cb47d5a5931888de41d59ca Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 12 Aug 2021 23:08:51 +0200 Subject: [PATCH 181/355] Updated ZHA to also poll Philips Hue lights with new firmware (#54513) --- homeassistant/components/zha/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 628d9c3b9be..a340ffae736 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -523,7 +523,7 @@ class Light(BaseLight, ZhaEntity): @STRICT_MATCH( channel_names=CHANNEL_ON_OFF, aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}, - manufacturers="Philips", + manufacturers={"Philips", "Signify Netherlands B.V."}, ) class HueLight(Light): """Representation of a HUE light which does not report attributes.""" From 50bcb3f821243e4cfac2706b2488d92f013db91f Mon Sep 17 00:00:00 2001 From: Gerard Date: Thu, 12 Aug 2021 23:33:02 +0200 Subject: [PATCH 182/355] Fix attributes not showing after using entity class attributes (#54558) --- .../bmw_connected_drive/__init__.py | 5 -- .../bmw_connected_drive/binary_sensor.py | 51 ++++++++----------- .../bmw_connected_drive/device_tracker.py | 1 + 3 files changed, 21 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 3bd2365f88e..17e57b5d09c 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -335,11 +335,6 @@ class BMWConnectedDriveBaseEntity(Entity): "manufacturer": vehicle.attributes.get("brand"), } - @property - def extra_state_attributes(self): - """Return the state attributes of the sensor.""" - return self._attrs - def update_callback(self): """Schedule a state update.""" self.schedule_update_ha_state(True) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index d7f0d150193..a7fd72fc1a7 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -85,54 +85,38 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): def update(self): """Read new state data from the library.""" vehicle_state = self._vehicle.state + result = self._attrs.copy() # device class opening: On means open, Off means closed if self._attribute == "lids": _LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed) - self._attr_state = not vehicle_state.all_lids_closed - if self._attribute == "windows": - self._attr_state = not vehicle_state.all_windows_closed - # device class lock: On means unlocked, Off means locked - if self._attribute == "door_lock_state": - # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED - self._attr_state = vehicle_state.door_lock_state not in [ - LockState.LOCKED, - LockState.SECURED, - ] - # device class light: On means light detected, Off means no light - if self._attribute == "lights_parking": - self._attr_state = vehicle_state.are_parking_lights_on - # device class problem: On means problem detected, Off means no problem - if self._attribute == "condition_based_services": - self._attr_state = not vehicle_state.are_all_cbs_ok - if self._attribute == "check_control_messages": - self._attr_state = vehicle_state.has_check_control_messages - # device class power: On means power detected, Off means no power - if self._attribute == "charging_status": - self._attr_state = vehicle_state.charging_status in [ChargingState.CHARGING] - # device class plug: On means device is plugged in, - # Off means device is unplugged - if self._attribute == "connection_status": - self._attr_state = vehicle_state.connection_status == "CONNECTED" - - vehicle_state = self._vehicle.state - result = self._attrs.copy() - - if self._attribute == "lids": + self._attr_is_on = not vehicle_state.all_lids_closed for lid in vehicle_state.lids: result[lid.name] = lid.state.value elif self._attribute == "windows": + self._attr_is_on = not vehicle_state.all_windows_closed for window in vehicle_state.windows: result[window.name] = window.state.value + # device class lock: On means unlocked, Off means locked elif self._attribute == "door_lock_state": + # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED + self._attr_is_on = vehicle_state.door_lock_state not in [ + LockState.LOCKED, + LockState.SECURED, + ] result["door_lock_state"] = vehicle_state.door_lock_state.value result["last_update_reason"] = vehicle_state.last_update_reason + # device class light: On means light detected, Off means no light elif self._attribute == "lights_parking": + self._attr_is_on = vehicle_state.are_parking_lights_on result["lights_parking"] = vehicle_state.parking_lights.value + # device class problem: On means problem detected, Off means no problem elif self._attribute == "condition_based_services": + self._attr_is_on = not vehicle_state.are_all_cbs_ok for report in vehicle_state.condition_based_services: result.update(self._format_cbs_report(report)) elif self._attribute == "check_control_messages": + self._attr_is_on = vehicle_state.has_check_control_messages check_control_messages = vehicle_state.check_control_messages has_check_control_messages = vehicle_state.has_check_control_messages if has_check_control_messages: @@ -142,13 +126,18 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): result["check_control_messages"] = cbs_list else: result["check_control_messages"] = "OK" + # device class power: On means power detected, Off means no power elif self._attribute == "charging_status": + self._attr_is_on = vehicle_state.charging_status in [ChargingState.CHARGING] result["charging_status"] = vehicle_state.charging_status.value result["last_charging_end_result"] = vehicle_state.last_charging_end_result + # device class plug: On means device is plugged in, + # Off means device is unplugged elif self._attribute == "connection_status": + self._attr_is_on = vehicle_state.connection_status == "CONNECTED" result["connection_status"] = vehicle_state.connection_status - self._attr_extra_state_attributes = sorted(result.items()) + self._attr_extra_state_attributes = result def _format_cbs_report(self, report): result = {} diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index 62b2ed9b9d9..c788051dc9a 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -59,6 +59,7 @@ class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity): def update(self): """Update state of the decvice tracker.""" + self._attr_extra_state_attributes = self._attrs self._location = ( self._vehicle.state.gps_position if self._vehicle.state.is_vehicle_tracking_enabled From b71a0c5d4bb22f1d7ec84e892cff851ad1d6f283 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 13 Aug 2021 00:17:12 +0000 Subject: [PATCH 183/355] [ci skip] Translation update --- .../components/sensor/translations/cs.json | 2 ++ .../components/sensor/translations/no.json | 2 ++ .../components/sensor/translations/ru.json | 2 ++ .../components/uptimerobot/translations/cs.json | 2 +- .../components/uptimerobot/translations/no.json | 13 ++++++++++++- .../xiaomi_miio/translations/select.cs.json | 7 +++++++ 6 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/xiaomi_miio/translations/select.cs.json diff --git a/homeassistant/components/sensor/translations/cs.json b/homeassistant/components/sensor/translations/cs.json index dfa2a263783..f493f4134ed 100644 --- a/homeassistant/components/sensor/translations/cs.json +++ b/homeassistant/components/sensor/translations/cs.json @@ -4,6 +4,7 @@ "is_battery_level": "Aktu\u00e1ln\u00ed \u00farove\u0148 nabit\u00ed baterie {entity_name}", "is_current": "Aktu\u00e1ln\u00ed proud {entity_name}", "is_energy": "Aktu\u00e1ln\u00ed energie {entity_name}", + "is_gas": "Aktu\u00e1ln\u00ed mno\u017estv\u00ed plynu {entity_name}", "is_humidity": "Aktu\u00e1ln\u00ed vlhkost {entity_name}", "is_illuminance": "Aktu\u00e1ln\u00ed osv\u011btlen\u00ed {entity_name}", "is_power": "Aktu\u00e1ln\u00ed v\u00fdkon {entity_name}", @@ -18,6 +19,7 @@ "battery_level": "P\u0159i zm\u011bn\u011b \u00farovn\u011b baterie {entity_name}", "current": "P\u0159i zm\u011bn\u011b proudu {entity_name}", "energy": "P\u0159i zm\u011bn\u011b energie {entity_name}", + "gas": "P\u0159i zm\u011bn\u011b mno\u017estv\u00ed plynu {entity_name}", "humidity": "P\u0159i zm\u011bn\u011b vlhkosti {entity_name}", "illuminance": "P\u0159i zm\u011bn\u011b osv\u011btlen\u00ed {entity_name}", "power": "P\u0159i zm\u011bn\u011b el. v\u00fdkonu {entity_name}", diff --git a/homeassistant/components/sensor/translations/no.json b/homeassistant/components/sensor/translations/no.json index 02204a4a49a..c9c9542b92b 100644 --- a/homeassistant/components/sensor/translations/no.json +++ b/homeassistant/components/sensor/translations/no.json @@ -6,6 +6,7 @@ "is_carbon_monoxide": "Gjeldende {entity_name} karbonmonoksid konsentrasjonsniv\u00e5", "is_current": "Gjeldende {entity_name} str\u00f8m", "is_energy": "Gjeldende {entity_name} effekt", + "is_gas": "Gjeldende {entity_name} gass", "is_humidity": "Gjeldende {entity_name} fuktighet", "is_illuminance": "Gjeldende {entity_name} belysningsstyrke", "is_power": "Gjeldende {entity_name}-effekt", @@ -22,6 +23,7 @@ "carbon_monoxide": "{entity_name} endringer i konsentrasjonen av karbonmonoksid", "current": "{entity_name} gjeldende endringer", "energy": "{entity_name} effektendringer", + "gas": "{entity_name} gass endres", "humidity": "{entity_name} fuktighets endringer", "illuminance": "{entity_name} belysningsstyrke endringer", "power": "{entity_name} effektendringer", diff --git a/homeassistant/components/sensor/translations/ru.json b/homeassistant/components/sensor/translations/ru.json index c44c9002fef..930459c4fc5 100644 --- a/homeassistant/components/sensor/translations/ru.json +++ b/homeassistant/components/sensor/translations/ru.json @@ -6,6 +6,7 @@ "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", "is_current": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u0438\u043b\u044b \u0442\u043e\u043a\u0430", "is_energy": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", + "is_gas": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_humidity": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_illuminance": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_power": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", @@ -22,6 +23,7 @@ "carbon_monoxide": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "current": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u0438\u043b\u044b \u0442\u043e\u043a\u0430", "energy": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", + "gas": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435 \u0433\u0430\u0437\u0430", "humidity": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "illuminance": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "power": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", diff --git a/homeassistant/components/uptimerobot/translations/cs.json b/homeassistant/components/uptimerobot/translations/cs.json index dc5ccb72741..fb6f0bfa70d 100644 --- a/homeassistant/components/uptimerobot/translations/cs.json +++ b/homeassistant/components/uptimerobot/translations/cs.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_configured": "\u00da\u010det je ji\u017e nastaven", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, diff --git a/homeassistant/components/uptimerobot/translations/no.json b/homeassistant/components/uptimerobot/translations/no.json index ee44ef0fdbc..8c6351d78c4 100644 --- a/homeassistant/components/uptimerobot/translations/no.json +++ b/homeassistant/components/uptimerobot/translations/no.json @@ -2,18 +2,29 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", + "reauth_failed_existing": "Kunne ikke oppdatere konfigurasjonsoppf\u00f8ringen. Fjern integrasjonen og sett den opp igjen.", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", "unknown": "Uventet feil" }, "error": { "cannot_connect": "Tilkobling mislyktes", "invalid_api_key": "Ugyldig API-n\u00f8kkel", + "reauth_failed_matching_account": "API-n\u00f8kkelen du oppgav, samsvarer ikke med konto-IDen for eksisterende konfigurasjon.", "unknown": "Uventet feil" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "description": "Du m\u00e5 angi en ny skrivebeskyttet API-n\u00f8kkel fra Uptime Robot", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "api_key": "API-n\u00f8kkel" - } + }, + "description": "Du m\u00e5 angi en skrivebeskyttet API-n\u00f8kkel fra Uptime Robot" } } } diff --git a/homeassistant/components/xiaomi_miio/translations/select.cs.json b/homeassistant/components/xiaomi_miio/translations/select.cs.json new file mode 100644 index 00000000000..d7f5e8b6c84 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.cs.json @@ -0,0 +1,7 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "off": "Vypnuto" + } + } +} \ No newline at end of file From 821b93b0d096c3622101caf32e29ef7cc8b8d8a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Fri, 13 Aug 2021 11:38:14 +0200 Subject: [PATCH 184/355] Fix bug in ambiclimate (#54579) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix ambiclimate Signed-off-by: Daniel Hjelseth Høyer * Fix ambiclimate Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/ambiclimate/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index 8cfebb1bf69..aa4be202865 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -154,8 +154,6 @@ class AmbiclimateEntity(ClimateEntity): "name": self.name, "manufacturer": "Ambiclimate", } - self._attr_min_temp = heater.get_min_temp() - self._attr_max_temp = heater.get_max_temp() async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" @@ -184,6 +182,8 @@ class AmbiclimateEntity(ClimateEntity): await self._store.async_save(token_info) data = await self._heater.update_device() + self._attr_min_temp = self._heater.get_min_temp() + self._attr_max_temp = self._heater.get_max_temp() self._attr_target_temperature = data.get("target_temperature") self._attr_current_temperature = data.get("temperature") self._attr_current_humidity = data.get("humidity") From 029873a0887e24583e46b3f456b88ee787b13210 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 13 Aug 2021 12:35:23 +0200 Subject: [PATCH 185/355] Add support for total and total_increasing sensor state classes (#54523) * Add support for amount and meter sensor state classes * Ignore last_reset for STATE_CLASS_METER sensors * Update tests * Rename STATE_CLASS_METER to STATE_CLASS_AMOUNT_INCREASING * Rename STATE_CLASS_AMOUNT to STATE_CLASS_TOTAL * Fix typo * Log warning if last_reset set together with state_class measurement * Fix warning message --- homeassistant/components/sensor/__init__.py | 30 +++- homeassistant/components/sensor/recorder.py | 93 +++++++--- tests/components/sensor/test_init.py | 22 +++ tests/components/sensor/test_recorder.py | 169 +++++++++++++++++- .../custom_components/test/sensor.py | 10 ++ 5 files changed, 296 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 483d8b88f2e..087328ed4a6 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -73,8 +73,16 @@ DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) # The state represents a measurement in present time STATE_CLASS_MEASUREMENT: Final = "measurement" +# The state represents a total amount, e.g. a value of a stock portfolio +STATE_CLASS_TOTAL: Final = "total" +# The state represents a monotonically increasing total, e.g. an amount of consumed gas +STATE_CLASS_TOTAL_INCREASING: Final = "total_increasing" -STATE_CLASSES: Final[list[str]] = [STATE_CLASS_MEASUREMENT] +STATE_CLASSES: Final[list[str]] = [ + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL, + STATE_CLASS_TOTAL_INCREASING, +] STATE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.In(STATE_CLASSES)) @@ -118,6 +126,7 @@ class SensorEntity(Entity): _attr_native_unit_of_measurement: str | None _attr_native_value: StateType = None _attr_state_class: str | None + _last_reset_reported = False _temperature_conversion_reported = False @property @@ -151,6 +160,25 @@ class SensorEntity(Entity): def state_attributes(self) -> dict[str, Any] | None: """Return state attributes.""" if last_reset := self.last_reset: + if ( + last_reset is not None + and self.state_class == STATE_CLASS_MEASUREMENT + and not self._last_reset_reported + ): + self._last_reset_reported = True + report_issue = self._suggest_report_issue() + _LOGGER.warning( + "Entity %s (%s) with state_class %s has set last_reset. Setting " + "last_reset for entities with state_class other than 'total' is " + "deprecated and will be removed from Home Assistant Core 2021.10. " + "Please update your configuration if state_class is manually " + "configured, otherwise %s", + self.entity_id, + type(self), + self.state_class, + report_issue, + ) + return {ATTR_LAST_RESET: last_reset.isoformat()} return None diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index fb7393cfe1d..66366934d27 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -17,6 +17,9 @@ from homeassistant.components.sensor import ( 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, @@ -50,15 +53,27 @@ from . import ATTR_LAST_RESET, DOMAIN _LOGGER = logging.getLogger(__name__) DEVICE_CLASS_OR_UNIT_STATISTICS = { - DEVICE_CLASS_BATTERY: {"mean", "min", "max"}, - DEVICE_CLASS_ENERGY: {"sum"}, - DEVICE_CLASS_HUMIDITY: {"mean", "min", "max"}, - DEVICE_CLASS_MONETARY: {"sum"}, - DEVICE_CLASS_POWER: {"mean", "min", "max"}, - DEVICE_CLASS_PRESSURE: {"mean", "min", "max"}, - DEVICE_CLASS_TEMPERATURE: {"mean", "min", "max"}, - DEVICE_CLASS_GAS: {"sum"}, - PERCENTAGE: {"mean", "min", "max"}, + STATE_CLASS_TOTAL: { + DEVICE_CLASS_ENERGY: {"sum"}, + DEVICE_CLASS_GAS: {"sum"}, + DEVICE_CLASS_MONETARY: {"sum"}, + }, + STATE_CLASS_MEASUREMENT: { + DEVICE_CLASS_BATTERY: {"mean", "min", "max"}, + DEVICE_CLASS_HUMIDITY: {"mean", "min", "max"}, + DEVICE_CLASS_POWER: {"mean", "min", "max"}, + DEVICE_CLASS_PRESSURE: {"mean", "min", "max"}, + DEVICE_CLASS_TEMPERATURE: {"mean", "min", "max"}, + PERCENTAGE: {"mean", "min", "max"}, + # Deprecated, support will be removed in Home Assistant 2021.10 + DEVICE_CLASS_ENERGY: {"sum"}, + DEVICE_CLASS_GAS: {"sum"}, + DEVICE_CLASS_MONETARY: {"sum"}, + }, + STATE_CLASS_TOTAL_INCREASING: { + DEVICE_CLASS_ENERGY: {"sum"}, + DEVICE_CLASS_GAS: {"sum"}, + }, } # Normalized units which will be stored in the statistics table @@ -109,24 +124,28 @@ UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { WARN_UNSUPPORTED_UNIT = set() -def _get_entities(hass: HomeAssistant) -> list[tuple[str, str]]: - """Get (entity_id, device_class) of all sensors for which to compile statistics.""" +def _get_entities(hass: HomeAssistant) -> list[tuple[str, str, str]]: + """Get (entity_id, state_class, key) of all sensors for which to compile statistics. + + Key is either a device class or a unit and is used to index the + DEVICE_CLASS_OR_UNIT_STATISTICS map. + """ all_sensors = hass.states.all(DOMAIN) entity_ids = [] for state in all_sensors: - if state.attributes.get(ATTR_STATE_CLASS) != STATE_CLASS_MEASUREMENT: + if (state_class := state.attributes.get(ATTR_STATE_CLASS)) not in STATE_CLASSES: continue if ( key := state.attributes.get(ATTR_DEVICE_CLASS) - ) in DEVICE_CLASS_OR_UNIT_STATISTICS: - entity_ids.append((state.entity_id, key)) + ) in DEVICE_CLASS_OR_UNIT_STATISTICS[state_class]: + entity_ids.append((state.entity_id, state_class, key)) if ( key := state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - ) in DEVICE_CLASS_OR_UNIT_STATISTICS: - entity_ids.append((state.entity_id, key)) + ) in DEVICE_CLASS_OR_UNIT_STATISTICS[state_class]: + entity_ids.append((state.entity_id, state_class, key)) return entity_ids @@ -228,8 +247,8 @@ def compile_statistics( hass, start - datetime.timedelta.resolution, end, [i[0] for i in entities] ) - for entity_id, key in entities: - wanted_statistics = DEVICE_CLASS_OR_UNIT_STATISTICS[key] + for entity_id, state_class, key in entities: + wanted_statistics = DEVICE_CLASS_OR_UNIT_STATISTICS[state_class][key] if entity_id not in history_list: continue @@ -272,9 +291,28 @@ def compile_statistics( for fstate, state in fstates: - if "last_reset" not in state.attributes: + # Deprecated, will be removed in Home Assistant 2021.10 + if ( + "last_reset" not in state.attributes + and state_class == STATE_CLASS_MEASUREMENT + ): continue - if (last_reset := state.attributes["last_reset"]) != old_last_reset: + + reset = False + if ( + state_class != STATE_CLASS_TOTAL_INCREASING + and (last_reset := state.attributes.get("last_reset")) + != old_last_reset + ): + reset = True + elif old_state is None and last_reset is None: + reset = True + elif state_class == STATE_CLASS_TOTAL_INCREASING and ( + old_state is None or fstate < old_state + ): + reset = True + + if reset: # The sensor has been reset, update the sum if old_state is not None: _sum += new_state - old_state @@ -285,14 +323,21 @@ def compile_statistics( else: new_state = fstate - if last_reset is None or new_state is None or old_state is None: + # Deprecated, will be removed in Home Assistant 2021.10 + if last_reset is None and state_class == STATE_CLASS_MEASUREMENT: + # No valid updates + result.pop(entity_id) + continue + + if new_state is None or old_state is None: # No valid updates result.pop(entity_id) continue # Update the sum with the last state _sum += new_state - old_state - stat["last_reset"] = dt_util.parse_datetime(last_reset) + if last_reset is not None: + stat["last_reset"] = dt_util.parse_datetime(last_reset) stat["sum"] = _sum stat["state"] = new_state @@ -307,8 +352,8 @@ def list_statistic_ids(hass: HomeAssistant, statistic_type: str | None = None) - statistic_ids = {} - for entity_id, key in entities: - provided_statistics = DEVICE_CLASS_OR_UNIT_STATISTICS[key] + for entity_id, state_class, key in entities: + provided_statistics = DEVICE_CLASS_OR_UNIT_STATISTICS[state_class][key] if statistic_type is not None and statistic_type not in provided_statistics: continue diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index f09cd489489..793bcaf4f99 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1,6 +1,7 @@ """The test for sensor device automation.""" from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util async def test_deprecated_temperature_conversion( @@ -28,3 +29,24 @@ async def test_deprecated_temperature_conversion( "your configuration if device_class is manually configured, otherwise report it " "to the custom component author." ) in caplog.text + + +async def test_deprecated_last_reset(hass, caplog, enable_custom_integrations): + """Test warning on deprecated last reset.""" + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", state_class="measurement", last_reset=dt_util.utc_from_timestamp(0) + ) + + assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) + await hass.async_block_till_done() + + assert ( + "Entity sensor.test () " + "with state_class measurement has set last_reset. Setting last_reset for " + "entities with state_class other than 'total' is deprecated and will be " + "removed from Home Assistant Core 2021.10. Please update your configuration if " + "state_class is manually configured, otherwise report it to the custom " + "component author." + ) in caplog.text diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index a612bc75a77..45d81e4b678 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -154,6 +154,7 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes assert "Error while processing event StatisticsTask" not in caplog.text +@pytest.mark.parametrize("state_class", ["measurement", "total"]) @pytest.mark.parametrize( "device_class,unit,native_unit,factor", [ @@ -165,8 +166,8 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes ("gas", "ft³", "m³", 0.0283168466), ], ) -def test_compile_hourly_sum_statistics( - hass_recorder, caplog, device_class, unit, native_unit, factor +def test_compile_hourly_sum_statistics_amount( + hass_recorder, caplog, state_class, device_class, unit, native_unit, factor ): """Test compiling hourly statistics.""" zero = dt_util.utcnow() @@ -175,7 +176,7 @@ def test_compile_hourly_sum_statistics( setup_component(hass, "sensor", {}) attributes = { "device_class": device_class, - "state_class": "measurement", + "state_class": state_class, "unit_of_measurement": unit, "last_reset": None, } @@ -237,6 +238,168 @@ def test_compile_hourly_sum_statistics( assert "Error while processing event StatisticsTask" not in caplog.text +@pytest.mark.parametrize( + "device_class,unit,native_unit,factor", + [ + ("energy", "kWh", "kWh", 1), + ("energy", "Wh", "kWh", 1 / 1000), + ("monetary", "EUR", "EUR", 1), + ("monetary", "SEK", "SEK", 1), + ("gas", "m³", "m³", 1), + ("gas", "ft³", "m³", 0.0283168466), + ], +) +def test_compile_hourly_sum_statistics_total_no_reset( + hass_recorder, caplog, device_class, unit, native_unit, factor +): + """Test compiling hourly statistics.""" + zero = dt_util.utcnow() + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + attributes = { + "device_class": device_class, + "state_class": "total", + "unit_of_measurement": unit, + } + seq = [10, 15, 20, 10, 30, 40, 50, 60, 70] + + four, eight, states = record_meter_states( + hass, zero, "sensor.test1", attributes, seq + ) + hist = history.get_significant_states( + hass, zero - timedelta.resolution, eight + timedelta.resolution + ) + assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"] + + recorder.do_adhoc_statistics(period="hourly", start=zero) + wait_recording_done(hass) + recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=1)) + wait_recording_done(hass) + recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=2)) + wait_recording_done(hass) + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + ] + stats = statistics_during_period(hass, zero) + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(factor * seq[2]), + "sum": approx(factor * 10.0), + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(factor * seq[5]), + "sum": approx(factor * 30.0), + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(factor * seq[8]), + "sum": approx(factor * 60.0), + }, + ] + } + assert "Error while processing event StatisticsTask" not in caplog.text + + +@pytest.mark.parametrize( + "device_class,unit,native_unit,factor", + [ + ("energy", "kWh", "kWh", 1), + ("energy", "Wh", "kWh", 1 / 1000), + ("gas", "m³", "m³", 1), + ("gas", "ft³", "m³", 0.0283168466), + ], +) +def test_compile_hourly_sum_statistics_total_increasing( + hass_recorder, caplog, device_class, unit, native_unit, factor +): + """Test compiling hourly statistics.""" + zero = dt_util.utcnow() + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + attributes = { + "device_class": device_class, + "state_class": "total_increasing", + "unit_of_measurement": unit, + } + seq = [10, 15, 20, 10, 30, 40, 50, 60, 70] + + four, eight, states = record_meter_states( + hass, zero, "sensor.test1", attributes, seq + ) + hist = history.get_significant_states( + hass, zero - timedelta.resolution, eight + timedelta.resolution + ) + assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"] + + recorder.do_adhoc_statistics(period="hourly", start=zero) + wait_recording_done(hass) + recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=1)) + wait_recording_done(hass) + recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=2)) + wait_recording_done(hass) + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + ] + stats = statistics_during_period(hass, zero) + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(factor * seq[2]), + "sum": approx(factor * 10.0), + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(factor * seq[5]), + "sum": approx(factor * 40.0), + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(factor * seq[8]), + "sum": approx(factor * 70.0), + }, + ] + } + assert "Error while processing event StatisticsTask" not in caplog.text + + def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): """Test compiling hourly statistics.""" zero = dt_util.utcnow() diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index f4b2e96321e..63f47a0f854 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -71,6 +71,11 @@ class MockSensor(MockEntity, sensor.SensorEntity): """Return the class of this sensor.""" return self._handle("device_class") + @property + def last_reset(self): + """Return the last_reset of this sensor.""" + return self._handle("last_reset") + @property def native_unit_of_measurement(self): """Return the native unit_of_measurement of this sensor.""" @@ -80,3 +85,8 @@ class MockSensor(MockEntity, sensor.SensorEntity): def native_value(self): """Return the native value of this sensor.""" return self._handle("native_value") + + @property + def state_class(self): + """Return the state class of this sensor.""" + return self._handle("state_class") From 3454102dc87bd5451cfddeadea4ba6e388dec8c7 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Fri, 13 Aug 2021 17:03:13 +0200 Subject: [PATCH 186/355] Fix for 'list index out of range' (#54588) --- homeassistant/components/solaredge_local/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index f9ac2b853e7..9d162e919f4 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -50,6 +50,7 @@ SENSOR_TYPES = { ELECTRIC_POTENTIAL_VOLT, "mdi:current-ac", None, + None, ], "current_DC_voltage": [ "dcvoltage", @@ -57,6 +58,7 @@ SENSOR_TYPES = { ELECTRIC_POTENTIAL_VOLT, "mdi:current-dc", None, + None, ], "current_frequency": [ "gridfrequency", From 2c1728022df0484d75033c82b219e5bfb061f918 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Fri, 13 Aug 2021 18:13:25 +0200 Subject: [PATCH 187/355] Use ssdp callbacks in upnp (#53840) --- .../components/dlna_dmr/manifest.json | 2 +- homeassistant/components/ssdp/__init__.py | 34 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/__init__.py | 66 ++- homeassistant/components/upnp/config_flow.py | 133 ++++-- homeassistant/components/upnp/const.py | 10 +- homeassistant/components/upnp/device.py | 45 -- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ssdp/test_init.py | 98 +++- tests/components/upnp/common.py | 23 + tests/components/upnp/mock_ssdp_scanner.py | 49 ++ .../{mock_device.py => mock_upnp_device.py} | 23 +- tests/components/upnp/test_config_flow.py | 429 +++++++----------- tests/components/upnp/test_init.py | 53 +-- 17 files changed, 531 insertions(+), 444 deletions(-) create mode 100644 tests/components/upnp/common.py create mode 100644 tests/components/upnp/mock_ssdp_scanner.py rename tests/components/upnp/{mock_device.py => mock_upnp_device.py} (77%) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index e9ac437fe46..1975128a8cc 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -2,7 +2,7 @@ "domain": "dlna_dmr", "name": "DLNA Digital Media Renderer", "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.19.1"], + "requirements": ["async-upnp-client==0.19.2"], "dependencies": ["network"], "codeowners": [], "iot_class": "local_push" diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 31ebb0d1a92..96bf47d920d 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -9,6 +9,7 @@ import logging from typing import Any, Callable from async_upnp_client.search import SSDPListener +from async_upnp_client.ssdp import SSDP_PORT from async_upnp_client.utils import CaseInsensitiveDict from homeassistant import config_entries @@ -228,6 +229,21 @@ class Scanner: for listener in self._ssdp_listeners: listener.async_search() + self.async_scan_broadcast() + + @core_callback + def async_scan_broadcast(self, *_: Any) -> None: + """Scan for new entries using broadcast target.""" + # Some sonos devices only seem to respond if we send to the broadcast + # address. This matches pysonos' behavior + # https://github.com/amelchio/pysonos/blob/d4329b4abb657d106394ae69357805269708c996/pysonos/discovery.py#L120 + for listener in self._ssdp_listeners: + try: + IPv4Address(listener.source_ip) + except ValueError: + continue + listener.async_search((str(IPV4_BROADCAST), SSDP_PORT)) + async def async_start(self) -> None: """Start the scanner.""" self.description_manager = DescriptionManager(self.hass) @@ -238,20 +254,6 @@ class Scanner: async_callback=self._async_process_entry, source_ip=source_ip ) ) - try: - IPv4Address(source_ip) - except ValueError: - continue - # Some sonos devices only seem to respond if we send to the broadcast - # address. This matches pysonos' behavior - # https://github.com/amelchio/pysonos/blob/d4329b4abb657d106394ae69357805269708c996/pysonos/discovery.py#L120 - self._ssdp_listeners.append( - SSDPListener( - async_callback=self._async_process_entry, - source_ip=source_ip, - target_ip=IPV4_BROADCAST, - ) - ) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STARTED, self.flow_dispatcher.async_start @@ -275,6 +277,10 @@ class Scanner: self.hass, self.async_scan, SCAN_INTERVAL ) + # Trigger a broadcast-scan. Regular scan is implicitly triggered + # by SSDPListener. + self.async_scan_broadcast() + @core_callback def _async_get_matching_callbacks( self, headers: Mapping[str, str] diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 432686d9027..ef4b92b4a14 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/ssdp", "requirements": [ "defusedxml==0.7.1", - "async-upnp-client==0.19.1" + "async-upnp-client==0.19.2" ], "dependencies": ["network"], "after_dependencies": ["zeroconf"], diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 6ad7111ae12..08e6a35f5b3 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -1,6 +1,10 @@ """Open ports in your router for Home Assistant and provide statistics.""" +from __future__ import annotations + import asyncio +from collections.abc import Mapping from ipaddress import ip_address +from typing import Any import voluptuous as vol @@ -9,7 +13,7 @@ from homeassistant.components import ssdp from homeassistant.components.network import async_get_source_ip from homeassistant.components.network.const import PUBLIC_TARGET_IP from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.typing import ConfigType @@ -44,21 +48,6 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_construct_device(hass: HomeAssistant, udn: str, st: str) -> Device: - """Discovery devices and construct a Device for one.""" - # pylint: disable=invalid-name - _LOGGER.debug("Constructing device: %s::%s", udn, st) - discovery_info = ssdp.async_get_discovery_info_by_udn_st(hass, udn, st) - - if not discovery_info: - _LOGGER.info("Device not discovered") - return None - - return await Device.async_create_device( - hass, discovery_info[ssdp.ATTR_SSDP_LOCATION] - ) - - async def async_setup(hass: HomeAssistant, config: ConfigType): """Set up UPnP component.""" _LOGGER.debug("async_setup, config: %s", config) @@ -86,20 +75,47 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up UPnP/IGD device from a config entry.""" _LOGGER.debug("Setting up config entry: %s", entry.unique_id) - # Discover and construct. udn = entry.data[CONFIG_ENTRY_UDN] st = entry.data[CONFIG_ENTRY_ST] # pylint: disable=invalid-name - try: - device = await async_construct_device(hass, udn, st) - except asyncio.TimeoutError as err: - raise ConfigEntryNotReady from err + usn = f"{udn}::{st}" - if not device: - _LOGGER.info("Unable to create UPnP/IGD, aborting") - raise ConfigEntryNotReady + # Register device discovered-callback. + device_discovered_event = asyncio.Event() + discovery_info: Mapping[str, Any] | None = None + + @callback + def device_discovered(info: Mapping[str, Any]) -> None: + nonlocal discovery_info + _LOGGER.debug( + "Device discovered: %s, at: %s", usn, info[ssdp.ATTR_SSDP_LOCATION] + ) + discovery_info = info + device_discovered_event.set() + + cancel_discovered_callback = ssdp.async_register_callback( + hass, + device_discovered, + { + "usn": usn, + }, + ) + + try: + await asyncio.wait_for(device_discovered_event.wait(), timeout=10) + except asyncio.TimeoutError as err: + _LOGGER.debug("Device not discovered: %s", usn) + raise ConfigEntryNotReady from err + finally: + cancel_discovered_callback() + + # Create device. + location = discovery_info[ # pylint: disable=unsubscriptable-object + ssdp.ATTR_SSDP_LOCATION + ] + device = await Device.async_create_device(hass, location) # Save device. - hass.data[DOMAIN][DOMAIN_DEVICES][device.udn] = device + hass.data[DOMAIN][DOMAIN_DEVICES][udn] = device # Ensure entry has a unique_id. if not entry.unique_id: diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 0679d9ffcb5..89e1e5c71d0 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -1,6 +1,7 @@ """Config flow for UPNP.""" from __future__ import annotations +import asyncio from collections.abc import Mapping from datetime import timedelta from typing import Any @@ -10,7 +11,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.const import CONF_SCAN_INTERVAL -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from .const import ( CONFIG_ENTRY_HOSTNAME, @@ -18,18 +19,70 @@ from .const import ( CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, DEFAULT_SCAN_INTERVAL, - DISCOVERY_HOSTNAME, - DISCOVERY_LOCATION, - DISCOVERY_NAME, - DISCOVERY_ST, - DISCOVERY_UDN, - DISCOVERY_UNIQUE_ID, - DISCOVERY_USN, DOMAIN, DOMAIN_DEVICES, LOGGER as _LOGGER, + SSDP_SEARCH_TIMEOUT, + ST_IGD_V1, + ST_IGD_V2, ) -from .device import Device, discovery_info_to_discovery + + +def _friendly_name_from_discovery(discovery_info: Mapping[str, Any]) -> str: + """Extract user-friendly name from discovery.""" + return ( + discovery_info.get("friendlyName") + or discovery_info.get("modeName") + or discovery_info.get("_host", "") + ) + + +async def _async_wait_for_discoveries(hass: HomeAssistant) -> bool: + """Wait for a device to be discovered.""" + device_discovered_event = asyncio.Event() + + @callback + def device_discovered(info: Mapping[str, Any]) -> None: + _LOGGER.info( + "Device discovered: %s, at: %s", + info[ssdp.ATTR_SSDP_USN], + info[ssdp.ATTR_SSDP_LOCATION], + ) + device_discovered_event.set() + + cancel_discovered_callback_1 = ssdp.async_register_callback( + hass, + device_discovered, + { + ssdp.ATTR_SSDP_ST: ST_IGD_V1, + }, + ) + cancel_discovered_callback_2 = ssdp.async_register_callback( + hass, + device_discovered, + { + ssdp.ATTR_SSDP_ST: ST_IGD_V2, + }, + ) + + try: + await asyncio.wait_for( + device_discovered_event.wait(), timeout=SSDP_SEARCH_TIMEOUT + ) + except asyncio.TimeoutError: + return False + finally: + cancel_discovered_callback_1() + cancel_discovered_callback_2() + + return True + + +def _discovery_igd_devices(hass: HomeAssistant) -> list[Mapping[str, Any]]: + """Discovery IGD devices.""" + return ssdp.async_get_discovery_info_by_st( + hass, ST_IGD_V1 + ) + ssdp.async_get_discovery_info_by_st(hass, ST_IGD_V2) class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -57,22 +110,19 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): matching_discoveries = [ discovery for discovery in self._discoveries - if discovery[DISCOVERY_UNIQUE_ID] == user_input["unique_id"] + if discovery[ssdp.ATTR_SSDP_USN] == user_input["unique_id"] ] if not matching_discoveries: return self.async_abort(reason="no_devices_found") discovery = matching_discoveries[0] await self.async_set_unique_id( - discovery[DISCOVERY_UNIQUE_ID], raise_on_progress=False + discovery[ssdp.ATTR_SSDP_USN], raise_on_progress=False ) return await self._async_create_entry_from_discovery(discovery) # Discover devices. - discoveries = [ - await Device.async_supplement_discovery(self.hass, discovery) - for discovery in await Device.async_discover(self.hass) - ] + discoveries = _discovery_igd_devices(self.hass) # Store discoveries which have not been configured. current_unique_ids = { @@ -81,7 +131,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._discoveries = [ discovery for discovery in discoveries - if discovery[DISCOVERY_UNIQUE_ID] not in current_unique_ids + if discovery[ssdp.ATTR_SSDP_USN] not in current_unique_ids ] # Ensure anything to add. @@ -92,7 +142,9 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): { vol.Required("unique_id"): vol.In( { - discovery[DISCOVERY_UNIQUE_ID]: discovery[DISCOVERY_NAME] + discovery[ssdp.ATTR_SSDP_USN]: _friendly_name_from_discovery( + discovery + ) for discovery in self._discoveries } ), @@ -119,27 +171,27 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_configured") # Discover devices. - self._discoveries = await Device.async_discover(self.hass) + await _async_wait_for_discoveries(self.hass) + discoveries = _discovery_igd_devices(self.hass) # Ensure anything to add. If not, silently abort. - if not self._discoveries: + if not discoveries: _LOGGER.info("No UPnP devices discovered, aborting") return self.async_abort(reason="no_devices_found") # Ensure complete discovery. - discovery = self._discoveries[0] + discovery = discoveries[0] if ( - DISCOVERY_UDN not in discovery - or DISCOVERY_ST not in discovery - or DISCOVERY_LOCATION not in discovery - or DISCOVERY_USN not in discovery + ssdp.ATTR_UPNP_UDN not in discovery + or ssdp.ATTR_SSDP_ST not in discovery + or ssdp.ATTR_SSDP_LOCATION not in discovery + or ssdp.ATTR_SSDP_USN not in discovery ): _LOGGER.debug("Incomplete discovery, ignoring") return self.async_abort(reason="incomplete_discovery") # Ensure not already configuring/configured. - discovery = await Device.async_supplement_discovery(self.hass, discovery) - unique_id = discovery[DISCOVERY_UNIQUE_ID] + unique_id = discovery[ssdp.ATTR_SSDP_USN] await self.async_set_unique_id(unique_id) return await self._async_create_entry_from_discovery(discovery) @@ -162,35 +214,28 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.debug("Incomplete discovery, ignoring") return self.async_abort(reason="incomplete_discovery") - # Convert to something we understand/speak. - discovery = discovery_info_to_discovery(discovery_info) - # Ensure not already configuring/configured. - unique_id = discovery[DISCOVERY_USN] + unique_id = discovery_info[ssdp.ATTR_SSDP_USN] await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured( - updates={CONFIG_ENTRY_HOSTNAME: discovery[DISCOVERY_HOSTNAME]} - ) + hostname = discovery_info["_host"] + self._abort_if_unique_id_configured(updates={CONFIG_ENTRY_HOSTNAME: hostname}) - # Handle devices changing their UDN, only allow a single + # Handle devices changing their UDN, only allow a single host. existing_entries = self._async_current_entries() for config_entry in existing_entries: entry_hostname = config_entry.data.get(CONFIG_ENTRY_HOSTNAME) - if entry_hostname == discovery[DISCOVERY_HOSTNAME]: + if entry_hostname == hostname: _LOGGER.debug( "Found existing config_entry with same hostname, discovery ignored" ) return self.async_abort(reason="discovery_ignored") - # Get more data about the device. - discovery = await Device.async_supplement_discovery(self.hass, discovery) - # Store discovery. - self._discoveries = [discovery] + self._discoveries = [discovery_info] # Ensure user recognizable. self.context["title_placeholders"] = { - "name": discovery[DISCOVERY_NAME], + "name": _friendly_name_from_discovery(discovery_info), } return await self.async_step_ssdp_confirm() @@ -224,11 +269,11 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): discovery, ) - title = discovery.get(DISCOVERY_NAME, "") + title = _friendly_name_from_discovery(discovery) data = { - CONFIG_ENTRY_UDN: discovery[DISCOVERY_UDN], - CONFIG_ENTRY_ST: discovery[DISCOVERY_ST], - CONFIG_ENTRY_HOSTNAME: discovery[DISCOVERY_HOSTNAME], + CONFIG_ENTRY_UDN: discovery["_udn"], + CONFIG_ENTRY_ST: discovery[ssdp.ATTR_SSDP_ST], + CONFIG_ENTRY_HOSTNAME: discovery["_host"], } return self.async_create_entry(title=title, data=data) diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index 0611176350a..cbb071bc15e 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -20,15 +20,11 @@ DATA_PACKETS = "packets" DATA_RATE_PACKETS_PER_SECOND = f"{DATA_PACKETS}/{TIME_SECONDS}" KIBIBYTE = 1024 UPDATE_INTERVAL = timedelta(seconds=30) -DISCOVERY_HOSTNAME = "hostname" -DISCOVERY_LOCATION = "location" -DISCOVERY_NAME = "name" -DISCOVERY_ST = "st" -DISCOVERY_UDN = "udn" -DISCOVERY_UNIQUE_ID = "unique_id" -DISCOVERY_USN = "usn" CONFIG_ENTRY_SCAN_INTERVAL = "scan_interval" CONFIG_ENTRY_ST = "st" CONFIG_ENTRY_UDN = "udn" CONFIG_ENTRY_HOSTNAME = "hostname" DEFAULT_SCAN_INTERVAL = timedelta(seconds=30).total_seconds() +ST_IGD_V1 = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" +ST_IGD_V2 = "urn:schemas-upnp-org:device:InternetGatewayDevice:2" +SSDP_SEARCH_TIMEOUT = 4 diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index cf76aa41f8a..5e6f8ef5023 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -12,7 +12,6 @@ from async_upnp_client.aiohttp import AiohttpSessionRequester from async_upnp_client.device_updater import DeviceUpdater from async_upnp_client.profiles.igd import IgdDevice -from homeassistant.components import ssdp from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -22,13 +21,6 @@ from .const import ( BYTES_RECEIVED, BYTES_SENT, CONF_LOCAL_IP, - DISCOVERY_HOSTNAME, - DISCOVERY_LOCATION, - DISCOVERY_NAME, - DISCOVERY_ST, - DISCOVERY_UDN, - DISCOVERY_UNIQUE_ID, - DISCOVERY_USN, DOMAIN, DOMAIN_CONFIG, LOGGER as _LOGGER, @@ -38,20 +30,6 @@ from .const import ( ) -def discovery_info_to_discovery(discovery_info: Mapping) -> Mapping: - """Convert a SSDP-discovery to 'our' discovery.""" - location = discovery_info[ssdp.ATTR_SSDP_LOCATION] - parsed = urlparse(location) - hostname = parsed.hostname - return { - DISCOVERY_UDN: discovery_info[ssdp.ATTR_UPNP_UDN], - DISCOVERY_ST: discovery_info[ssdp.ATTR_SSDP_ST], - DISCOVERY_LOCATION: discovery_info[ssdp.ATTR_SSDP_LOCATION], - DISCOVERY_USN: discovery_info[ssdp.ATTR_SSDP_USN], - DISCOVERY_HOSTNAME: hostname, - } - - def _get_local_ip(hass: HomeAssistant) -> IPv4Address | None: """Get the configured local ip.""" if DOMAIN in hass.data and DOMAIN_CONFIG in hass.data[DOMAIN]: @@ -70,29 +48,6 @@ class Device: self._device_updater = device_updater self.coordinator: DataUpdateCoordinator = None - @classmethod - async def async_discover(cls, hass: HomeAssistant) -> list[Mapping]: - """Discover UPnP/IGD devices.""" - _LOGGER.debug("Discovering UPnP/IGD devices") - discoveries = [] - for ssdp_st in IgdDevice.DEVICE_TYPES: - for discovery_info in ssdp.async_get_discovery_info_by_st(hass, ssdp_st): - discoveries.append(discovery_info_to_discovery(discovery_info)) - return discoveries - - @classmethod - async def async_supplement_discovery( - cls, hass: HomeAssistant, discovery: Mapping - ) -> Mapping: - """Get additional data from device and supplement discovery.""" - location = discovery[DISCOVERY_LOCATION] - device = await Device.async_create_device(hass, location) - discovery[DISCOVERY_NAME] = device.name - discovery[DISCOVERY_HOSTNAME] = device.hostname - discovery[DISCOVERY_UNIQUE_ID] = discovery[DISCOVERY_USN] - - return discovery - @classmethod async def async_create_device( cls, hass: HomeAssistant, ssdp_location: str diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 41d50b4bae8..937518c34ac 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.19.1"], + "requirements": ["async-upnp-client==0.19.2"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman"], "ssdp": [ diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 323b1c86034..4c3aca7f2dd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.2 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.19.1 +async-upnp-client==0.19.2 async_timeout==3.0.1 attrs==21.2.0 awesomeversion==21.4.0 diff --git a/requirements_all.txt b/requirements_all.txt index 76a9b4f7543..1045ec26c62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -311,7 +311,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp # homeassistant.components.upnp -async-upnp-client==0.19.1 +async-upnp-client==0.19.2 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 988c2bfb2c0..aaf5771e559 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -202,7 +202,7 @@ arcam-fmj==0.7.0 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp # homeassistant.components.upnp -async-upnp-client==0.19.1 +async-upnp-client==0.19.2 # homeassistant.components.aurora auroranoaa==0.0.2 diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 34ca1b7228e..94cf8a58908 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -29,7 +29,13 @@ def _patched_ssdp_listener(info, *args, **kwargs): async def _async_callback(*_): await listener.async_callback(info) + @callback + def _async_search(*_): + # Prevent an actual scan. + pass + listener.async_start = _async_callback + listener.async_search = _async_search return listener @@ -287,7 +293,10 @@ async def test_invalid_characters(hass, aioclient_mock): @patch("homeassistant.components.ssdp.SSDPListener.async_start") @patch("homeassistant.components.ssdp.SSDPListener.async_search") -async def test_start_stop_scanner(async_start_mock, async_search_mock, hass): +@patch("homeassistant.components.ssdp.SSDPListener.async_stop") +async def test_start_stop_scanner( + async_stop_mock, async_search_mock, async_start_mock, hass +): """Test we start and stop the scanner.""" assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}}) @@ -295,15 +304,18 @@ async def test_start_stop_scanner(async_start_mock, async_search_mock, hass): await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) await hass.async_block_till_done() - assert async_start_mock.call_count == 2 - assert async_search_mock.call_count == 2 + assert async_start_mock.call_count == 1 + # Next is 3, as async_upnp_client triggers 1 SSDPListener._async_on_connect + assert async_search_mock.call_count == 3 + assert async_stop_mock.call_count == 0 hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) await hass.async_block_till_done() - assert async_start_mock.call_count == 2 - assert async_search_mock.call_count == 2 + assert async_start_mock.call_count == 1 + assert async_search_mock.call_count == 3 + assert async_stop_mock.call_count == 1 async def test_unexpected_exception_while_fetching(hass, aioclient_mock, caplog): @@ -787,7 +799,6 @@ async def test_async_detect_interfaces_setting_empty_route(hass): assert argset == { (IPv6Address("2001:db8::"), None), - (IPv4Address("192.168.1.5"), IPv4Address("255.255.255.255")), (IPv4Address("192.168.1.5"), None), } @@ -802,12 +813,12 @@ async def test_bind_failure_skips_adapter(hass, caplog): ] } create_args = [] - did_search = 0 + search_args = [] @callback - def _callback(*_): - nonlocal did_search - did_search += 1 + def _callback(*args): + nonlocal search_args + search_args.append(args) pass def _generate_failing_ssdp_listener(*args, **kwargs): @@ -844,11 +855,74 @@ async def test_bind_failure_skips_adapter(hass, caplog): assert argset == { (IPv6Address("2001:db8::"), None), - (IPv4Address("192.168.1.5"), IPv4Address("255.255.255.255")), (IPv4Address("192.168.1.5"), None), } assert "Failed to setup listener for" in caplog.text async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) await hass.async_block_till_done() - assert did_search == 2 + assert set(search_args) == { + (), + ( + ( + "255.255.255.255", + 1900, + ), + ), + } + + +async def test_ipv4_does_additional_search_for_sonos(hass, caplog): + """Test that only ipv4 does an additional search for Sonos.""" + mock_get_ssdp = { + "mock-domain": [ + { + ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC", + } + ] + } + search_args = [] + + def _generate_fake_ssdp_listener(*args, **kwargs): + listener = SSDPListener(*args, **kwargs) + + async def _async_callback(*_): + pass + + @callback + def _callback(*args): + nonlocal search_args + search_args.append(args) + pass + + listener.async_start = _async_callback + listener.async_search = _callback + return listener + + with patch( + "homeassistant.components.ssdp.async_get_ssdp", + return_value=mock_get_ssdp, + ), patch( + "homeassistant.components.ssdp.SSDPListener", + new=_generate_fake_ssdp_listener, + ), patch( + "homeassistant.components.ssdp.network.async_get_adapters", + return_value=_ADAPTERS_WITH_MANUAL_CONFIG, + ): + assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) + await hass.async_block_till_done() + + assert set(search_args) == { + (), + ( + ( + "255.255.255.255", + 1900, + ), + ), + } diff --git a/tests/components/upnp/common.py b/tests/components/upnp/common.py new file mode 100644 index 00000000000..4dd0fd4083d --- /dev/null +++ b/tests/components/upnp/common.py @@ -0,0 +1,23 @@ +"""Common for upnp.""" + +from urllib.parse import urlparse + +from homeassistant.components import ssdp + +TEST_UDN = "uuid:device" +TEST_ST = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" +TEST_USN = f"{TEST_UDN}::{TEST_ST}" +TEST_LOCATION = "http://192.168.1.1/desc.xml" +TEST_HOSTNAME = urlparse(TEST_LOCATION).hostname +TEST_FRIENDLY_NAME = "friendly name" +TEST_DISCOVERY = { + ssdp.ATTR_SSDP_LOCATION: TEST_LOCATION, + ssdp.ATTR_SSDP_ST: TEST_ST, + ssdp.ATTR_SSDP_USN: TEST_USN, + ssdp.ATTR_UPNP_UDN: TEST_UDN, + "usn": TEST_USN, + "location": TEST_LOCATION, + "_host": TEST_HOSTNAME, + "_udn": TEST_UDN, + "friendlyName": TEST_FRIENDLY_NAME, +} diff --git a/tests/components/upnp/mock_ssdp_scanner.py b/tests/components/upnp/mock_ssdp_scanner.py new file mode 100644 index 00000000000..39f9a801bb6 --- /dev/null +++ b/tests/components/upnp/mock_ssdp_scanner.py @@ -0,0 +1,49 @@ +"""Mock ssdp.Scanner.""" +from __future__ import annotations + +from typing import Any +from unittest.mock import patch + +import pytest + +from homeassistant.components import ssdp +from homeassistant.core import callback + + +class MockSsdpDescriptionManager(ssdp.DescriptionManager): + """Mocked ssdp DescriptionManager.""" + + async def fetch_description( + self, xml_location: str | None + ) -> None | dict[str, str]: + """Fetch the location or get it from the cache.""" + if xml_location is None: + return None + return {} + + +class MockSsdpScanner(ssdp.Scanner): + """Mocked ssdp Scanner.""" + + @callback + def async_stop(self, *_: Any) -> None: + """Stop the scanner.""" + # Do nothing. + + async def async_start(self) -> None: + """Start the scanner.""" + self.description_manager = MockSsdpDescriptionManager(self.hass) + + @callback + def async_scan(self, *_: Any) -> None: + """Scan for new entries.""" + # Do nothing. + + +@pytest.fixture +def mock_ssdp_scanner(): + """Mock ssdp Scanner.""" + with patch( + "homeassistant.components.ssdp.Scanner", new=MockSsdpScanner + ) as mock_ssdp_scanner: + yield mock_ssdp_scanner diff --git a/tests/components/upnp/mock_device.py b/tests/components/upnp/mock_upnp_device.py similarity index 77% rename from tests/components/upnp/mock_device.py rename to tests/components/upnp/mock_upnp_device.py index 7161ae69598..78adbc5e220 100644 --- a/tests/components/upnp/mock_device.py +++ b/tests/components/upnp/mock_upnp_device.py @@ -1,7 +1,9 @@ """Mock device for testing purposes.""" from typing import Any, Mapping -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch + +import pytest from homeassistant.components.upnp.const import ( BYTES_RECEIVED, @@ -13,6 +15,8 @@ from homeassistant.components.upnp.const import ( from homeassistant.components.upnp.device import Device from homeassistant.util import dt +from .common import TEST_UDN + class MockDevice(Device): """Mock device for Device.""" @@ -28,7 +32,7 @@ class MockDevice(Device): @classmethod async def async_create_device(cls, hass, ssdp_location) -> "MockDevice": """Return self.""" - return cls("UDN") + return cls(TEST_UDN) @property def udn(self) -> str: @@ -70,3 +74,18 @@ class MockDevice(Device): PACKETS_RECEIVED: 0, PACKETS_SENT: 0, } + + async def async_start(self) -> None: + """Start the device updater.""" + + async def async_stop(self) -> None: + """Stop the device updater.""" + + +@pytest.fixture +def mock_upnp_device(): + """Mock upnp Device.async_create_device.""" + with patch( + "homeassistant.components.upnp.Device", new=MockDevice + ) as mock_async_create_device: + yield mock_async_create_device diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index 6e546be93f3..646bdb143e9 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -1,8 +1,9 @@ """Test UPnP/IGD config flow.""" from datetime import timedelta -from unittest.mock import AsyncMock, Mock, patch -from urllib.parse import urlparse +from unittest.mock import patch + +import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp @@ -12,119 +13,92 @@ from homeassistant.components.upnp.const import ( CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, DEFAULT_SCAN_INTERVAL, - DISCOVERY_HOSTNAME, - DISCOVERY_LOCATION, - DISCOVERY_NAME, - DISCOVERY_ST, - DISCOVERY_UDN, - DISCOVERY_UNIQUE_ID, - DISCOVERY_USN, DOMAIN, + DOMAIN_DEVICES, ) -from homeassistant.components.upnp.device import Device -from homeassistant.core import HomeAssistant +from homeassistant.core import CoreState, HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util import dt -from .mock_device import MockDevice +from .common import ( + TEST_DISCOVERY, + TEST_FRIENDLY_NAME, + TEST_HOSTNAME, + TEST_LOCATION, + TEST_ST, + TEST_UDN, + TEST_USN, +) +from .mock_ssdp_scanner import mock_ssdp_scanner # noqa: F401 +from .mock_upnp_device import mock_upnp_device # noqa: F401 from tests.common import MockConfigEntry, async_fire_time_changed -async def test_flow_ssdp_discovery(hass: HomeAssistant): +@pytest.mark.usefixtures("mock_ssdp_scanner", "mock_upnp_device") +async def test_flow_ssdp_discovery( + hass: HomeAssistant, +): """Test config flow: discovered + configured through ssdp.""" - udn = "uuid:device_1" - location = "http://dummy" - mock_device = MockDevice(udn) - ssdp_discoveries = [ - { - ssdp.ATTR_SSDP_LOCATION: location, - ssdp.ATTR_SSDP_ST: mock_device.device_type, - ssdp.ATTR_UPNP_UDN: mock_device.udn, - ssdp.ATTR_SSDP_USN: mock_device.usn, - } - ] - discoveries = [ - { - DISCOVERY_LOCATION: location, - DISCOVERY_NAME: mock_device.name, - DISCOVERY_ST: mock_device.device_type, - DISCOVERY_UDN: mock_device.udn, - DISCOVERY_UNIQUE_ID: mock_device.unique_id, - DISCOVERY_USN: mock_device.usn, - DISCOVERY_HOSTNAME: mock_device.hostname, - } - ] - with patch.object( - Device, "async_create_device", AsyncMock(return_value=mock_device) - ), patch.object( - ssdp, "async_get_discovery_info_by_st", Mock(return_value=ssdp_discoveries) - ), patch.object( - Device, "async_supplement_discovery", AsyncMock(return_value=discoveries[0]) - ): - # Discovered via step ssdp. - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: location, - ssdp.ATTR_SSDP_ST: mock_device.device_type, - ssdp.ATTR_SSDP_USN: mock_device.usn, - ssdp.ATTR_UPNP_UDN: mock_device.udn, - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "ssdp_confirm" - - # Confirm via step ssdp_confirm. - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == mock_device.name - assert result["data"] == { - CONFIG_ENTRY_ST: mock_device.device_type, - CONFIG_ENTRY_UDN: mock_device.udn, - CONFIG_ENTRY_HOSTNAME: mock_device.hostname, - } - - -async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistant): - """Test config flow: incomplete discovery through ssdp.""" - udn = "uuid:device_1" - location = "http://dummy" - mock_device = MockDevice(udn) + # Ensure we have a ssdp Scanner. + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + ssdp_scanner: ssdp.Scanner = hass.data[ssdp.DOMAIN] + ssdp_scanner.cache[(TEST_UDN, TEST_ST)] = TEST_DISCOVERY + # Speed up callback in ssdp.async_register_callback. + hass.state = CoreState.not_running + # Discovered via step ssdp. + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=TEST_DISCOVERY, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "ssdp_confirm" + + # Confirm via step ssdp_confirm. + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == TEST_FRIENDLY_NAME + assert result["data"] == { + CONFIG_ENTRY_ST: TEST_ST, + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_HOSTNAME: TEST_HOSTNAME, + } + + +@pytest.mark.usefixtures("mock_ssdp_scanner") +async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistant): + """Test config flow: incomplete discovery through ssdp.""" # Discovered via step ssdp. result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data={ - ssdp.ATTR_SSDP_LOCATION: location, - ssdp.ATTR_SSDP_ST: mock_device.device_type, - ssdp.ATTR_SSDP_USN: mock_device.usn, - # ssdp.ATTR_UPNP_UDN: mock_device.udn, # Not provided. + ssdp.ATTR_SSDP_LOCATION: TEST_LOCATION, + ssdp.ATTR_SSDP_ST: TEST_ST, + ssdp.ATTR_SSDP_USN: TEST_USN, + # ssdp.ATTR_UPNP_UDN: TEST_UDN, # Not provided. }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "incomplete_discovery" +@pytest.mark.usefixtures("mock_ssdp_scanner") async def test_flow_ssdp_discovery_ignored(hass: HomeAssistant): """Test config flow: discovery through ssdp, but ignored, as hostname is used by existing config entry.""" - udn = "uuid:device_random_1" - location = "http://dummy" - mock_device = MockDevice(udn) - # Existing entry. config_entry = MockConfigEntry( domain=DOMAIN, data={ - CONFIG_ENTRY_UDN: "uuid:device_random_2", - CONFIG_ENTRY_ST: mock_device.device_type, - CONFIG_ENTRY_HOSTNAME: urlparse(location).hostname, + CONFIG_ENTRY_UDN: TEST_UDN + "2", + CONFIG_ENTRY_ST: TEST_ST, + CONFIG_ENTRY_HOSTNAME: TEST_HOSTNAME, }, options={CONFIG_ENTRY_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, ) @@ -134,129 +108,78 @@ async def test_flow_ssdp_discovery_ignored(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_LOCATION: location, - ssdp.ATTR_SSDP_ST: mock_device.device_type, - ssdp.ATTR_SSDP_USN: mock_device.usn, - ssdp.ATTR_UPNP_UDN: mock_device.udn, - }, + data=TEST_DISCOVERY, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "discovery_ignored" +@pytest.mark.usefixtures("mock_ssdp_scanner", "mock_upnp_device") async def test_flow_user(hass: HomeAssistant): """Test config flow: discovered + configured through user.""" - udn = "uuid:device_1" - location = "http://dummy" - mock_device = MockDevice(udn) - ssdp_discoveries = [ - { - ssdp.ATTR_SSDP_LOCATION: location, - ssdp.ATTR_SSDP_ST: mock_device.device_type, - ssdp.ATTR_UPNP_UDN: mock_device.udn, - ssdp.ATTR_SSDP_USN: mock_device.usn, - } - ] - discoveries = [ - { - DISCOVERY_LOCATION: location, - DISCOVERY_NAME: mock_device.name, - DISCOVERY_ST: mock_device.device_type, - DISCOVERY_UDN: mock_device.udn, - DISCOVERY_UNIQUE_ID: mock_device.unique_id, - DISCOVERY_USN: mock_device.usn, - DISCOVERY_HOSTNAME: mock_device.hostname, - } - ] + # Ensure we have a ssdp Scanner. + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + ssdp_scanner: ssdp.Scanner = hass.data[ssdp.DOMAIN] + ssdp_scanner.cache[(TEST_UDN, TEST_ST)] = TEST_DISCOVERY + # Speed up callback in ssdp.async_register_callback. + hass.state = CoreState.not_running - with patch.object( - Device, "async_create_device", AsyncMock(return_value=mock_device) - ), patch.object( - ssdp, "async_get_discovery_info_by_st", Mock(return_value=ssdp_discoveries) - ), patch.object( - Device, "async_supplement_discovery", AsyncMock(return_value=discoveries[0]) - ): - # Discovered via step user. - 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" + # Discovered via step user. + 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" - # Confirmed via step user. - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={"unique_id": mock_device.unique_id}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == mock_device.name - assert result["data"] == { - CONFIG_ENTRY_ST: mock_device.device_type, - CONFIG_ENTRY_UDN: mock_device.udn, - CONFIG_ENTRY_HOSTNAME: mock_device.hostname, - } + # Confirmed via step user. + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"unique_id": TEST_USN}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == TEST_FRIENDLY_NAME + assert result["data"] == { + CONFIG_ENTRY_ST: TEST_ST, + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_HOSTNAME: TEST_HOSTNAME, + } +@pytest.mark.usefixtures("mock_ssdp_scanner", "mock_upnp_device") async def test_flow_import(hass: HomeAssistant): - """Test config flow: discovered + configured through configuration.yaml.""" - udn = "uuid:device_1" - mock_device = MockDevice(udn) - location = "http://dummy" - ssdp_discoveries = [ - { - ssdp.ATTR_SSDP_LOCATION: location, - ssdp.ATTR_SSDP_ST: mock_device.device_type, - ssdp.ATTR_UPNP_UDN: mock_device.udn, - ssdp.ATTR_SSDP_USN: mock_device.usn, - } - ] - discoveries = [ - { - DISCOVERY_LOCATION: location, - DISCOVERY_NAME: mock_device.name, - DISCOVERY_ST: mock_device.device_type, - DISCOVERY_UDN: mock_device.udn, - DISCOVERY_UNIQUE_ID: mock_device.unique_id, - DISCOVERY_USN: mock_device.usn, - DISCOVERY_HOSTNAME: mock_device.hostname, - } - ] + """Test config flow: configured through configuration.yaml.""" + # Ensure we have a ssdp Scanner. + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + ssdp_scanner: ssdp.Scanner = hass.data[ssdp.DOMAIN] + ssdp_scanner.cache[(TEST_UDN, TEST_ST)] = TEST_DISCOVERY + # Speed up callback in ssdp.async_register_callback. + hass.state = CoreState.not_running - with patch.object( - Device, "async_create_device", AsyncMock(return_value=mock_device) - ), patch.object( - ssdp, "async_get_discovery_info_by_st", Mock(return_value=ssdp_discoveries) - ), patch.object( - Device, "async_supplement_discovery", AsyncMock(return_value=discoveries[0]) - ): - # Discovered via step import. - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == mock_device.name - assert result["data"] == { - CONFIG_ENTRY_ST: mock_device.device_type, - CONFIG_ENTRY_UDN: mock_device.udn, - CONFIG_ENTRY_HOSTNAME: mock_device.hostname, - } + # Discovered via step import. + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == TEST_FRIENDLY_NAME + assert result["data"] == { + CONFIG_ENTRY_ST: TEST_ST, + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_HOSTNAME: TEST_HOSTNAME, + } +@pytest.mark.usefixtures("mock_ssdp_scanner") async def test_flow_import_already_configured(hass: HomeAssistant): - """Test config flow: discovered, but already configured.""" - udn = "uuid:device_1" - mock_device = MockDevice(udn) - + """Test config flow: configured through configuration.yaml, but existing config entry.""" # Existing entry. config_entry = MockConfigEntry( domain=DOMAIN, data={ - CONFIG_ENTRY_UDN: mock_device.udn, - CONFIG_ENTRY_ST: mock_device.device_type, - CONFIG_ENTRY_HOSTNAME: mock_device.hostname, + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_ST: TEST_ST, + CONFIG_ENTRY_HOSTNAME: TEST_HOSTNAME, }, options={CONFIG_ENTRY_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, ) @@ -271,94 +194,88 @@ async def test_flow_import_already_configured(hass: HomeAssistant): assert result["reason"] == "already_configured" +@pytest.mark.usefixtures("mock_ssdp_scanner") async def test_flow_import_no_devices_found(hass: HomeAssistant): """Test config flow: no devices found, configured through configuration.yaml.""" - ssdp_discoveries = [] - with patch.object( - ssdp, "async_get_discovery_info_by_st", Mock(return_value=ssdp_discoveries) + # Ensure we have a ssdp Scanner. + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + ssdp_scanner: ssdp.Scanner = hass.data[ssdp.DOMAIN] + ssdp_scanner.cache.clear() + + # Discovered via step import. + with patch( + "homeassistant.components.upnp.config_flow.SSDP_SEARCH_TIMEOUT", new=0.0 ): - # Discovered via step import. result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "no_devices_found" +@pytest.mark.usefixtures("mock_ssdp_scanner", "mock_upnp_device") async def test_options_flow(hass: HomeAssistant): """Test options flow.""" + # Ensure we have a ssdp Scanner. + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + ssdp_scanner: ssdp.Scanner = hass.data[ssdp.DOMAIN] + ssdp_scanner.cache[(TEST_UDN, TEST_ST)] = TEST_DISCOVERY + # Speed up callback in ssdp.async_register_callback. + hass.state = CoreState.not_running + # Set up config entry. - udn = "uuid:device_1" - location = "http://192.168.1.1/desc.xml" - mock_device = MockDevice(udn) - ssdp_discoveries = [ - { - ssdp.ATTR_SSDP_LOCATION: location, - ssdp.ATTR_SSDP_ST: mock_device.device_type, - ssdp.ATTR_UPNP_UDN: mock_device.udn, - ssdp.ATTR_SSDP_USN: mock_device.usn, - } - ] config_entry = MockConfigEntry( domain=DOMAIN, data={ - CONFIG_ENTRY_UDN: mock_device.udn, - CONFIG_ENTRY_ST: mock_device.device_type, - CONFIG_ENTRY_HOSTNAME: mock_device.hostname, + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_ST: TEST_ST, + CONFIG_ENTRY_HOSTNAME: TEST_HOSTNAME, }, options={CONFIG_ENTRY_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, ) config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) is True + await hass.async_block_till_done() + mock_device = hass.data[DOMAIN][DOMAIN_DEVICES][TEST_UDN] - config = { - # no upnp, ensures no import-flow is started. + # Reset. + mock_device.times_polled = 0 + + # Forward time, ensure single poll after 30 (default) seconds. + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=31)) + await hass.async_block_till_done() + assert mock_device.times_polled == 1 + + # Options flow with no input results in form. + result = await hass.config_entries.options.async_init( + config_entry.entry_id, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + # Options flow with input results in update to entry. + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONFIG_ENTRY_SCAN_INTERVAL: 60}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + CONFIG_ENTRY_SCAN_INTERVAL: 60, } - with patch.object( - Device, "async_create_device", AsyncMock(return_value=mock_device) - ), patch.object( - ssdp, - "async_get_discovery_info_by_udn_st", - Mock(return_value=ssdp_discoveries[0]), - ): - # Initialisation of component. - await async_setup_component(hass, "upnp", config) - await hass.async_block_till_done() - mock_device.times_polled = 0 # Reset. - # Forward time, ensure single poll after 30 (default) seconds. - async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=31)) - await hass.async_block_till_done() - assert mock_device.times_polled == 1 + # Forward time, ensure single poll after 60 seconds, still from original setting. + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=61)) + await hass.async_block_till_done() + assert mock_device.times_polled == 2 - # Options flow with no input results in form. - result = await hass.config_entries.options.async_init( - config_entry.entry_id, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + # Now the updated interval takes effect. + # Forward time, ensure single poll after 120 seconds. + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=121)) + await hass.async_block_till_done() + assert mock_device.times_polled == 3 - # Options flow with input results in update to entry. - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={CONFIG_ENTRY_SCAN_INTERVAL: 60}, - ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert config_entry.options == { - CONFIG_ENTRY_SCAN_INTERVAL: 60, - } - - # Forward time, ensure single poll after 60 seconds, still from original setting. - async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=61)) - await hass.async_block_till_done() - assert mock_device.times_polled == 2 - - # Now the updated interval takes effect. - # Forward time, ensure single poll after 120 seconds. - async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=121)) - await hass.async_block_till_done() - assert mock_device.times_polled == 3 - - # Forward time, ensure single poll after 180 seconds. - async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=181)) - await hass.async_block_till_done() - assert mock_device.times_polled == 4 + # Forward time, ensure single poll after 180 seconds. + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=181)) + await hass.async_block_till_done() + assert mock_device.times_polled == 4 diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 0770906f0da..9ccdbf02f4b 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -1,6 +1,7 @@ """Test UPnP/IGD setup process.""" +from __future__ import annotations -from unittest.mock import AsyncMock, Mock, patch +import pytest from homeassistant.components import ssdp from homeassistant.components.upnp.const import ( @@ -8,51 +9,37 @@ from homeassistant.components.upnp.const import ( CONFIG_ENTRY_UDN, DOMAIN, ) -from homeassistant.components.upnp.device import Device -from homeassistant.core import HomeAssistant +from homeassistant.core import CoreState, HomeAssistant from homeassistant.setup import async_setup_component -from .mock_device import MockDevice +from .common import TEST_DISCOVERY, TEST_ST, TEST_UDN +from .mock_ssdp_scanner import mock_ssdp_scanner # noqa: F401 +from .mock_upnp_device import mock_upnp_device # noqa: F401 from tests.common import MockConfigEntry +@pytest.mark.usefixtures("mock_ssdp_scanner", "mock_upnp_device") async def test_async_setup_entry_default(hass: HomeAssistant): """Test async_setup_entry.""" - udn = "uuid:device_1" - location = "http://192.168.1.1/desc.xml" - mock_device = MockDevice(udn) - discovery = { - ssdp.ATTR_SSDP_LOCATION: location, - ssdp.ATTR_SSDP_ST: mock_device.device_type, - ssdp.ATTR_UPNP_UDN: mock_device.udn, - ssdp.ATTR_SSDP_USN: mock_device.usn, - } entry = MockConfigEntry( domain=DOMAIN, data={ - CONFIG_ENTRY_UDN: mock_device.udn, - CONFIG_ENTRY_ST: mock_device.device_type, + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_ST: TEST_ST, }, ) - config = { - # no upnp - } - async_create_device = AsyncMock(return_value=mock_device) - mock_get_discovery = Mock() - with patch.object(Device, "async_create_device", async_create_device), patch.object( - ssdp, "async_get_discovery_info_by_udn_st", mock_get_discovery - ): - # initialisation of component, no device discovered - mock_get_discovery.return_value = None - await async_setup_component(hass, "upnp", config) - await hass.async_block_till_done() + # Initialisation of component, no device discovered. + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() - # loading of config_entry, device discovered - mock_get_discovery.return_value = discovery - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) is True + # Device is discovered. + ssdp_scanner: ssdp.Scanner = hass.data[ssdp.DOMAIN] + ssdp_scanner.cache[(TEST_UDN, TEST_ST)] = TEST_DISCOVERY + # Speed up callback in ssdp.async_register_callback. + hass.state = CoreState.not_running - # ensure device is stored/used - async_create_device.assert_called_with(hass, discovery[ssdp.ATTR_SSDP_LOCATION]) + # Load config_entry. + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) is True From eb278834de6527a395aaa560fcebb5b996f89923 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 13 Aug 2021 19:39:16 +0200 Subject: [PATCH 188/355] Add gas support to energy (#54560) Co-authored-by: Paulus Schoutsen --- homeassistant/components/energy/data.py | 31 ++- homeassistant/components/energy/sensor.py | 225 ++++++++++++++-------- tests/components/energy/test_sensor.py | 47 +++++ 3 files changed, 220 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/energy/data.py b/homeassistant/components/energy/data.py index 9196694953a..1cea20564b4 100644 --- a/homeassistant/components/energy/data.py +++ b/homeassistant/components/energy/data.py @@ -88,7 +88,25 @@ class BatterySourceType(TypedDict): stat_energy_to: str -SourceType = Union[GridSourceType, SolarSourceType, BatterySourceType] +class GasSourceType(TypedDict): + """Dictionary holding the source of gas storage.""" + + type: Literal["gas"] + + stat_energy_from: str + + # statistic_id of costs ($) incurred from the energy meter + # If set to None and entity_energy_from and entity_energy_price are configured, + # an EnergyCostSensor will be automatically created + stat_cost: str | None + + # Used to generate costs if stat_cost is set to None + entity_energy_from: str | None # entity_id of an gas meter (m³), entity_id of the gas meter for stat_energy_from + entity_energy_price: str | None # entity_id of an entity providing price ($/m³) + number_energy_price: float | None # Price for energy ($/m³) + + +SourceType = Union[GridSourceType, SolarSourceType, BatterySourceType, GasSourceType] class DeviceConsumption(TypedDict): @@ -193,6 +211,16 @@ BATTERY_SOURCE_SCHEMA = vol.Schema( vol.Required("stat_energy_to"): str, } ) +GAS_SOURCE_SCHEMA = vol.Schema( + { + vol.Required("type"): "gas", + vol.Required("stat_energy_from"): str, + vol.Optional("stat_cost"): vol.Any(str, None), + vol.Optional("entity_energy_from"): vol.Any(str, None), + vol.Optional("entity_energy_price"): vol.Any(str, None), + vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None), + } +) def check_type_limits(value: list[SourceType]) -> list[SourceType]: @@ -214,6 +242,7 @@ ENERGY_SOURCE_SCHEMA = vol.All( "grid": GRID_SOURCE_SCHEMA, "solar": SOLAR_SOURCE_SCHEMA, "battery": BATTERY_SOURCE_SCHEMA, + "gas": GAS_SOURCE_SCHEMA, }, ) ] diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index ccf1a0d7b34..fd36611acaf 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -from functools import partial import logging from typing import Any, Final, Literal, TypeVar, cast @@ -16,6 +15,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, + VOLUME_CUBIC_METERS, ) from homeassistant.core import HomeAssistant, State, callback, split_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -36,22 +36,19 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the energy sensors.""" - manager = await async_get_manager(hass) - process_now = partial(_process_manager_data, hass, manager, async_add_entities, {}) - manager.async_listen_updates(process_now) - - if manager.data: - await process_now() + sensor_manager = SensorManager(await async_get_manager(hass), async_add_entities) + await sensor_manager.async_start() T = TypeVar("T") @dataclass -class FlowAdapter: - """Adapter to allow flows to be used as sensors.""" +class SourceAdapter: + """Adapter to allow sources and their flows to be used as sensors.""" - flow_type: Literal["flow_from", "flow_to"] + source_type: Literal["grid", "gas"] + flow_type: Literal["flow_from", "flow_to", None] stat_energy_key: Literal["stat_energy_from", "stat_energy_to"] entity_energy_key: Literal["entity_energy_from", "entity_energy_to"] total_money_key: Literal["stat_cost", "stat_compensation"] @@ -59,8 +56,9 @@ class FlowAdapter: entity_id_suffix: str -FLOW_ADAPTERS: Final = ( - FlowAdapter( +SOURCE_ADAPTERS: Final = ( + SourceAdapter( + "grid", "flow_from", "stat_energy_from", "entity_energy_from", @@ -68,7 +66,8 @@ FLOW_ADAPTERS: Final = ( "Cost", "cost", ), - FlowAdapter( + SourceAdapter( + "grid", "flow_to", "stat_energy_to", "entity_energy_to", @@ -76,67 +75,112 @@ FLOW_ADAPTERS: Final = ( "Compensation", "compensation", ), + SourceAdapter( + "gas", + None, + "stat_energy_from", + "entity_energy_from", + "stat_cost", + "Cost", + "cost", + ), ) -async def _process_manager_data( - hass: HomeAssistant, - manager: EnergyManager, - async_add_entities: AddEntitiesCallback, - current_entities: dict[tuple[str, str], EnergyCostSensor], -) -> None: - """Process updated data.""" - to_add: list[SensorEntity] = [] - to_remove = dict(current_entities) +class SensorManager: + """Class to handle creation/removal of sensor data.""" - async def finish() -> None: - if to_add: - async_add_entities(to_add) + def __init__( + self, manager: EnergyManager, async_add_entities: AddEntitiesCallback + ) -> None: + """Initialize sensor manager.""" + self.manager = manager + self.async_add_entities = async_add_entities + self.current_entities: dict[tuple[str, str | None, str], EnergyCostSensor] = {} - for key, entity in to_remove.items(): - current_entities.pop(key) - await entity.async_remove() + async def async_start(self) -> None: + """Start.""" + self.manager.async_listen_updates(self._process_manager_data) + + if self.manager.data: + await self._process_manager_data() + + async def _process_manager_data(self) -> None: + """Process manager data.""" + to_add: list[SensorEntity] = [] + to_remove = dict(self.current_entities) + + async def finish() -> None: + if to_add: + self.async_add_entities(to_add) + + for key, entity in to_remove.items(): + self.current_entities.pop(key) + await entity.async_remove() + + if not self.manager.data: + await finish() + return + + for energy_source in self.manager.data["energy_sources"]: + for adapter in SOURCE_ADAPTERS: + if adapter.source_type != energy_source["type"]: + continue + + if adapter.flow_type is None: + self._process_sensor_data( + adapter, + # Opting out of the type complexity because can't get it to work + energy_source, # type: ignore + to_add, + to_remove, + ) + continue + + for flow in energy_source[adapter.flow_type]: # type: ignore + self._process_sensor_data( + adapter, + # Opting out of the type complexity because can't get it to work + flow, # type: ignore + to_add, + to_remove, + ) - if not manager.data: await finish() - return - for energy_source in manager.data["energy_sources"]: - if energy_source["type"] != "grid": - continue + @callback + def _process_sensor_data( + self, + adapter: SourceAdapter, + config: dict, + to_add: list[SensorEntity], + to_remove: dict[tuple[str, str | None, str], EnergyCostSensor], + ) -> None: + """Process sensor data.""" + # No need to create an entity if we already have a cost stat + if config.get(adapter.total_money_key) is not None: + return - for adapter in FLOW_ADAPTERS: - for flow in energy_source[adapter.flow_type]: - # Opting out of the type complexity because can't get it to work - untyped_flow = cast(dict, flow) + key = (adapter.source_type, adapter.flow_type, config[adapter.stat_energy_key]) - # No need to create an entity if we already have a cost stat - if untyped_flow.get(adapter.total_money_key) is not None: - continue + # Make sure the right data is there + # If the entity existed, we don't pop it from to_remove so it's removed + if config.get(adapter.entity_energy_key) is None or ( + config.get("entity_energy_price") is None + and config.get("number_energy_price") is None + ): + return - # This is unique among all flow_from's - key = (adapter.flow_type, untyped_flow[adapter.stat_energy_key]) + current_entity = to_remove.pop(key, None) + if current_entity: + current_entity.update_config(config) + return - # Make sure the right data is there - # If the entity existed, we don't pop it from to_remove so it's removed - if untyped_flow.get(adapter.entity_energy_key) is None or ( - untyped_flow.get("entity_energy_price") is None - and untyped_flow.get("number_energy_price") is None - ): - continue - - current_entity = to_remove.pop(key, None) - if current_entity: - current_entity.update_config(untyped_flow) - continue - - current_entities[key] = EnergyCostSensor( - adapter, - untyped_flow, - ) - to_add.append(current_entities[key]) - - await finish() + self.current_entities[key] = EnergyCostSensor( + adapter, + config, + ) + to_add.append(self.current_entities[key]) class EnergyCostSensor(SensorEntity): @@ -148,17 +192,19 @@ class EnergyCostSensor(SensorEntity): def __init__( self, - adapter: FlowAdapter, - flow: dict, + adapter: SourceAdapter, + config: dict, ) -> None: """Initialize the sensor.""" super().__init__() self._adapter = adapter - self.entity_id = f"{flow[adapter.entity_energy_key]}_{adapter.entity_id_suffix}" + 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_MEASUREMENT - self._flow = flow + self._config = config self._last_energy_sensor_state: State | None = None self._cur_value = 0.0 @@ -174,7 +220,7 @@ class EnergyCostSensor(SensorEntity): def _update_cost(self) -> None: """Update incurred costs.""" energy_state = self.hass.states.get( - cast(str, self._flow[self._adapter.entity_energy_key]) + cast(str, self._config[self._adapter.entity_energy_key]) ) if energy_state is None or ATTR_LAST_RESET not in energy_state.attributes: @@ -186,8 +232,10 @@ class EnergyCostSensor(SensorEntity): return # Determine energy price - if self._flow["entity_energy_price"] is not None: - energy_price_state = self.hass.states.get(self._flow["entity_energy_price"]) + if self._config["entity_energy_price"] is not None: + energy_price_state = self.hass.states.get( + self._config["entity_energy_price"] + ) if energy_price_state is None: return @@ -197,14 +245,17 @@ class EnergyCostSensor(SensorEntity): except ValueError: return - if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith( - f"/{ENERGY_WATT_HOUR}" + if ( + self._adapter.source_type == "grid" + and energy_price_state.attributes.get( + ATTR_UNIT_OF_MEASUREMENT, "" + ).endswith(f"/{ENERGY_WATT_HOUR}") ): energy_price *= 1000.0 else: energy_price_state = None - energy_price = cast(float, self._flow["number_energy_price"]) + energy_price = cast(float, self._config["number_energy_price"]) if self._last_energy_sensor_state is None: # Initialize as it's the first time all required entities are in place. @@ -213,9 +264,17 @@ class EnergyCostSensor(SensorEntity): energy_unit = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if energy_unit == ENERGY_WATT_HOUR: - energy_price /= 1000 - elif energy_unit != ENERGY_KILO_WATT_HOUR: + if self._adapter.source_type == "grid": + if energy_unit == ENERGY_WATT_HOUR: + energy_price /= 1000 + elif energy_unit != ENERGY_KILO_WATT_HOUR: + energy_unit = None + + elif self._adapter.source_type == "gas": + if energy_unit != VOLUME_CUBIC_METERS: + energy_unit = None + + if energy_unit is None: _LOGGER.warning( "Found unexpected unit %s for %s", energy_unit, energy_state.entity_id ) @@ -237,11 +296,13 @@ class EnergyCostSensor(SensorEntity): async def async_added_to_hass(self) -> None: """Register callbacks.""" - energy_state = self.hass.states.get(self._flow[self._adapter.entity_energy_key]) + energy_state = self.hass.states.get( + self._config[self._adapter.entity_energy_key] + ) if energy_state: name = energy_state.name else: - name = split_entity_id(self._flow[self._adapter.entity_energy_key])[ + name = split_entity_id(self._config[self._adapter.entity_energy_key])[ 0 ].replace("_", " ") @@ -251,7 +312,7 @@ class EnergyCostSensor(SensorEntity): # Store stat ID in hass.data so frontend can look it up self.hass.data[DOMAIN]["cost_sensors"][ - self._flow[self._adapter.entity_energy_key] + self._config[self._adapter.entity_energy_key] ] = self.entity_id @callback @@ -263,7 +324,7 @@ class EnergyCostSensor(SensorEntity): self.async_on_remove( async_track_state_change_event( self.hass, - cast(str, self._flow[self._adapter.entity_energy_key]), + cast(str, self._config[self._adapter.entity_energy_key]), async_state_changed_listener, ) ) @@ -271,14 +332,14 @@ class EnergyCostSensor(SensorEntity): async def async_will_remove_from_hass(self) -> None: """Handle removing from hass.""" self.hass.data[DOMAIN]["cost_sensors"].pop( - self._flow[self._adapter.entity_energy_key] + self._config[self._adapter.entity_energy_key] ) await super().async_will_remove_from_hass() @callback - def update_config(self, flow: dict) -> None: + def update_config(self, config: dict) -> None: """Update the config.""" - self._flow = flow + self._config = config @property def native_unit_of_measurement(self) -> str | None: diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index 978b21e1919..1e89c05fbd6 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -18,6 +18,7 @@ from homeassistant.const import ( DEVICE_CLASS_MONETARY, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, + VOLUME_CUBIC_METERS, ) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -295,3 +296,49 @@ async def test_cost_sensor_handle_wh(hass, hass_storage) -> None: state = hass.states.get("sensor.energy_consumption_cost") assert state.state == "5.0" + + +async def test_cost_sensor_handle_gas(hass, hass_storage) -> None: + """Test gas cost price from sensor entity.""" + energy_data = data.EnergyManager.default_preferences() + energy_data["energy_sources"].append( + { + "type": "gas", + "stat_energy_from": "sensor.gas_consumption", + "entity_energy_from": "sensor.gas_consumption", + "stat_cost": None, + "entity_energy_price": None, + "number_energy_price": 0.5, + } + ) + + hass_storage[data.STORAGE_KEY] = { + "version": 1, + "data": energy_data, + } + + now = dt_util.utcnow() + last_reset = dt_util.utc_from_timestamp(0).isoformat() + + hass.states.async_set( + "sensor.gas_consumption", + 100, + {"last_reset": last_reset, "unit_of_measurement": VOLUME_CUBIC_METERS}, + ) + + with patch("homeassistant.util.dt.utcnow", return_value=now): + await setup_integration(hass) + + state = hass.states.get("sensor.gas_consumption_cost") + assert state.state == "0.0" + + # gas use bumped to 10 kWh + hass.states.async_set( + "sensor.gas_consumption", + 200, + {"last_reset": last_reset, "unit_of_measurement": VOLUME_CUBIC_METERS}, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.gas_consumption_cost") + assert state.state == "50.0" From 8264fd2eb68c3e6c06f8c173583fb9c355673f82 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 13 Aug 2021 20:48:31 +0200 Subject: [PATCH 189/355] Update frontend to 20210813.0 (#54603) --- 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 135c0ec0244..a4a97914622 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==20210809.0" + "home-assistant-frontend==20210813.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4c3aca7f2dd..bbfc9a20381 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.46.0 -home-assistant-frontend==20210809.0 +home-assistant-frontend==20210813.0 httpx==0.18.2 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 1045ec26c62..0522b51a14f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -786,7 +786,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210809.0 +home-assistant-frontend==20210813.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aaf5771e559..a1ed1bb69ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -452,7 +452,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210809.0 +home-assistant-frontend==20210813.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From f4fb5f2f5a2f7e3f69ecff295c09b22f688a85da Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 13 Aug 2021 15:42:55 -0500 Subject: [PATCH 190/355] Skip Sonos zeroconf availability check in non-timeout scenarios (#54425) --- homeassistant/components/sonos/speaker.py | 25 ++++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 6e887dfdc53..9485d5dcff3 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -499,21 +499,26 @@ class SonosSpeaker: self.async_write_entity_states() - async def async_unseen(self, now: datetime.datetime | None = None) -> None: + async def async_unseen( + self, callback_timestamp: datetime.datetime | None = None + ) -> None: """Make this player unavailable when it was not seen recently.""" if self._seen_timer: self._seen_timer() self._seen_timer = None - hostname = uid_to_short_hostname(self.soco.uid) - zcname = f"{hostname}.{MDNS_SERVICE}" - aiozeroconf = await zeroconf.async_get_async_instance(self.hass) - if await aiozeroconf.async_get_service_info(MDNS_SERVICE, zcname): - # We can still see the speaker via zeroconf check again later. - self._seen_timer = self.hass.helpers.event.async_call_later( - SEEN_EXPIRE_TIME.total_seconds(), self.async_unseen - ) - return + if callback_timestamp: + # Called by a _seen_timer timeout, check mDNS one more time + # This should not be checked in an "active" unseen scenario + hostname = uid_to_short_hostname(self.soco.uid) + zcname = f"{hostname}.{MDNS_SERVICE}" + aiozeroconf = await zeroconf.async_get_async_instance(self.hass) + if await aiozeroconf.async_get_service_info(MDNS_SERVICE, zcname): + # We can still see the speaker via zeroconf check again later. + self._seen_timer = self.hass.helpers.event.async_call_later( + SEEN_EXPIRE_TIME.total_seconds(), self.async_unseen + ) + return _LOGGER.debug( "No activity and could not locate %s on the network. Marking unavailable", From 370b7f387da6706348d0c0a121c711e782e47d89 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 14 Aug 2021 00:11:27 +0000 Subject: [PATCH 191/355] [ci skip] Translation update --- .../components/adax/translations/cs.json | 1 + .../airvisual/translations/sensor.cs.json | 20 +++++++++++++++++++ .../alarm_control_panel/translations/fr.json | 1 + .../components/coinbase/translations/cs.json | 7 +++++++ .../forecast_solar/translations/cs.json | 13 ++++++++++++ .../nmap_tracker/translations/cs.json | 10 ++++++++++ .../components/sensor/translations/ca.json | 2 ++ .../uptimerobot/translations/cs.json | 6 +++++- .../components/zwave_js/translations/cs.json | 15 ++++++++++++++ 9 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/airvisual/translations/sensor.cs.json create mode 100644 homeassistant/components/forecast_solar/translations/cs.json diff --git a/homeassistant/components/adax/translations/cs.json b/homeassistant/components/adax/translations/cs.json index ce5fa77543f..1d090f44de2 100644 --- a/homeassistant/components/adax/translations/cs.json +++ b/homeassistant/components/adax/translations/cs.json @@ -10,6 +10,7 @@ "step": { "user": { "data": { + "account_id": "ID \u00fa\u010dtu", "host": "Hostitel", "password": "Heslo" } diff --git a/homeassistant/components/airvisual/translations/sensor.cs.json b/homeassistant/components/airvisual/translations/sensor.cs.json new file mode 100644 index 00000000000..44c834c7df6 --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.cs.json @@ -0,0 +1,20 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "Oxid uhelnat\u00fd", + "n2": "Oxid dusi\u010dit\u00fd", + "o3": "Oz\u00f3n", + "p1": "PM10", + "p2": "PM2,5", + "s2": "Oxid si\u0159i\u010dit\u00fd" + }, + "airvisual__pollutant_level": { + "good": "Dobr\u00e9", + "hazardous": "Riskantn\u00ed", + "moderate": "M\u00edrn\u00e9", + "unhealthy": "Nezdrav\u00e9", + "unhealthy_sensitive": "Nezdrav\u00e9 pro citliv\u00e9 skupiny", + "very_unhealthy": "Velmi nezdrav\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/translations/fr.json b/homeassistant/components/alarm_control_panel/translations/fr.json index 6d8ee9c08c3..bbcb26f7184 100644 --- a/homeassistant/components/alarm_control_panel/translations/fr.json +++ b/homeassistant/components/alarm_control_panel/translations/fr.json @@ -12,6 +12,7 @@ "is_armed_away": "{entity_name} est arm\u00e9", "is_armed_home": "{entity_name} est arm\u00e9 \u00e0 la maison", "is_armed_night": "{entity_name} est arm\u00e9 la nuit", + "is_armed_vacation": "{entity_name} est arm\u00e9 en mode vacances", "is_disarmed": "{entity_name} est d\u00e9sarm\u00e9", "is_triggered": "{entity_name} est d\u00e9clench\u00e9" }, diff --git a/homeassistant/components/coinbase/translations/cs.json b/homeassistant/components/coinbase/translations/cs.json index 32a69bfe33d..c6f6a1f36f9 100644 --- a/homeassistant/components/coinbase/translations/cs.json +++ b/homeassistant/components/coinbase/translations/cs.json @@ -19,6 +19,13 @@ "options": { "error": { "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "init": { + "data": { + "exchange_base": "Z\u00e1kladn\u00ed m\u011bna pro senzory sm\u011bnn\u00fdch kurz\u016f." + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/forecast_solar/translations/cs.json b/homeassistant/components/forecast_solar/translations/cs.json new file mode 100644 index 00000000000..0b970643bbe --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/cs.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", + "name": "Jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/cs.json b/homeassistant/components/nmap_tracker/translations/cs.json index 1a0d0ae0b53..ac5f913d8e6 100644 --- a/homeassistant/components/nmap_tracker/translations/cs.json +++ b/homeassistant/components/nmap_tracker/translations/cs.json @@ -3,5 +3,15 @@ "abort": { "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno" } + }, + "options": { + "step": { + "init": { + "data": { + "interval_seconds": "Interval skenov\u00e1n\u00ed", + "track_new_devices": "Sledovat nov\u00e1 za\u0159\u00edzen\u00ed" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/ca.json b/homeassistant/components/sensor/translations/ca.json index a30748d5c9c..f0df998170d 100644 --- a/homeassistant/components/sensor/translations/ca.json +++ b/homeassistant/components/sensor/translations/ca.json @@ -6,6 +6,7 @@ "is_carbon_monoxide": "Concentraci\u00f3 actual de mon\u00f2xid de carboni de {entity_name}", "is_current": "Intensitat actual de {entity_name}", "is_energy": "Energia actual de {entity_name}", + "is_gas": "Gas actual de {entity_name}", "is_humidity": "Humitat actual de {entity_name}", "is_illuminance": "Il\u00b7luminaci\u00f3 actual de {entity_name}", "is_power": "Pot\u00e8ncia actual de {entity_name}", @@ -22,6 +23,7 @@ "carbon_monoxide": "Canvia la concentraci\u00f3 de mon\u00f2xid de carboni de {entity_name}", "current": "Canvia la intensitat de {entity_name}", "energy": "Canvia l'energia de {entity_name}", + "gas": "Canvia el gas de {entity_name}", "humidity": "Canvia la humitat de {entity_name}", "illuminance": "Canvia la il\u00b7luminaci\u00f3 de {entity_name}", "power": "Canvia la pot\u00e8ncia de {entity_name}", diff --git a/homeassistant/components/uptimerobot/translations/cs.json b/homeassistant/components/uptimerobot/translations/cs.json index fb6f0bfa70d..09480693834 100644 --- a/homeassistant/components/uptimerobot/translations/cs.json +++ b/homeassistant/components/uptimerobot/translations/cs.json @@ -2,12 +2,14 @@ "config": { "abort": { "already_configured": "\u00da\u010det je ji\u017e nastaven", + "reauth_failed_existing": "Nepoda\u0159ilo se aktualizovat polo\u017eku konfigurace, odstra\u0148te pros\u00edm integraci a nastavte ji znovu.", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API", + "reauth_failed_matching_account": "Zadan\u00fd kl\u00ed\u010d API neodpov\u00edd\u00e1 ID \u00fa\u010dtu pro st\u00e1vaj\u00edc\u00ed konfiguraci.", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { @@ -15,12 +17,14 @@ "data": { "api_key": "Kl\u00ed\u010d API" }, + "description": "Je t\u0159eba zadat nov\u00fd kl\u00ed\u010d API \u010dten\u00ed od spole\u010dnosti Uptime Robot.", "title": "Znovu ov\u011b\u0159it integraci" }, "user": { "data": { "api_key": "Kl\u00ed\u010d API" - } + }, + "description": "Mus\u00edte zadat kl\u00ed\u010d API pro \u010dten\u00ed od spole\u010dnosti Uptime Robot." } } } diff --git a/homeassistant/components/zwave_js/translations/cs.json b/homeassistant/components/zwave_js/translations/cs.json index 9f8af44c451..05efdb8e5ff 100644 --- a/homeassistant/components/zwave_js/translations/cs.json +++ b/homeassistant/components/zwave_js/translations/cs.json @@ -21,5 +21,20 @@ } } } + }, + "device_automation": { + "condition_type": { + "config_parameter": "Hodnota konfigura\u010dn\u00edho parametru {subtype}", + "node_status": "Stav uzlu", + "value": "Aktu\u00e1ln\u00ed hodnota Z-Wave hodnoty" + }, + "trigger_type": { + "event.notification.entry_control": "Odeslat ozn\u00e1men\u00ed o \u0159\u00edzen\u00ed vstupu", + "event.notification.notification": "Odeslal ozn\u00e1men\u00ed", + "event.value_notification.basic": "Z\u00e1kladn\u00ed ud\u00e1lost CC na {subtype}", + "event.value_notification.central_scene": "Akce centr\u00e1ln\u00ed sc\u00e9ny na {subtype}", + "event.value_notification.scene_activation": "Aktivace sc\u00e9ny na {subtype}", + "state.node_status": "Stav uzlu zm\u011bn\u011bn" + } } } \ No newline at end of file From c10497d49961a007a9106b9a9039e529a4ba62c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Aug 2021 01:26:57 -0500 Subject: [PATCH 192/355] Bump zeroconf to 0.35.0 (#54604) Fixes https://github.com/home-assistant/core/issues/54531 Fixes https://github.com/home-assistant/core/issues/54434 Fixes https://github.com/home-assistant/core/issues/54487 --- 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 83db312601c..b971ec06179 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.34.3"], + "requirements": ["zeroconf==0.35.0"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bbfc9a20381..3f7eeb65ab5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ sqlalchemy==1.4.17 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 -zeroconf==0.34.3 +zeroconf==0.35.0 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 0522b51a14f..bb2d174bc23 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2439,7 +2439,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.34.3 +zeroconf==0.35.0 # homeassistant.components.zha zha-quirks==0.0.59 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a1ed1bb69ad..ca2b5e79894 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1347,7 +1347,7 @@ youless-api==0.10 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.34.3 +zeroconf==0.35.0 # homeassistant.components.zha zha-quirks==0.0.59 From 2c181181e15a4a45168d4e185f1309f63043a867 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Sat, 14 Aug 2021 08:27:47 +0200 Subject: [PATCH 193/355] Clamp color temperature to supported range in ESPHome light (#54595) ESPHome devices initially report a color temperature of 0 or 1 until it has been changed by the user. This broke the conversion from RGBWW to an RGB color. Fixes #54293. --- homeassistant/components/esphome/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index b89a75ab76a..aeecc22d9f1 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -230,7 +230,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): # Try to reverse white + color temp to cwww min_ct = self._static_info.min_mireds max_ct = self._static_info.max_mireds - color_temp = self._state.color_temperature + color_temp = min(max(self._state.color_temperature, min_ct), max_ct) white = self._state.white ww_frac = (color_temp - min_ct) / (max_ct - min_ct) From 102789672a699f1f579f1669967da5ce03f662de Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sat, 14 Aug 2021 08:38:42 +0200 Subject: [PATCH 194/355] Bump python-miio to 0.5.7 (#54601) --- 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 1f37d624b95..6d3c5e50be8 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.3", "python-miio==0.5.6"], + "requirements": ["construct==2.10.56", "micloud==0.3", "python-miio==0.5.7"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG"], "zeroconf": ["_miio._udp.local."], "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index bb2d174bc23..09a67edb29c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1860,7 +1860,7 @@ python-juicenet==1.0.2 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.5.6 +python-miio==0.5.7 # homeassistant.components.mpd python-mpd2==3.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ca2b5e79894..44ab676e9f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1041,7 +1041,7 @@ python-izone==1.1.6 python-juicenet==1.0.2 # homeassistant.components.xiaomi_miio -python-miio==0.5.6 +python-miio==0.5.7 # homeassistant.components.nest python-nest==4.1.0 From 08d8b026d00cec09fc2b03552710f53d21b3809b Mon Sep 17 00:00:00 2001 From: Colin O'Dell Date: Sat, 14 Aug 2021 02:44:52 -0400 Subject: [PATCH 195/355] Upgrade qnapstats library to 0.4.0 (#54571) --- homeassistant/components/qnap/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/qnap/manifest.json b/homeassistant/components/qnap/manifest.json index abd5d6f5a4a..217d14a6adf 100644 --- a/homeassistant/components/qnap/manifest.json +++ b/homeassistant/components/qnap/manifest.json @@ -2,7 +2,7 @@ "domain": "qnap", "name": "QNAP", "documentation": "https://www.home-assistant.io/integrations/qnap", - "requirements": ["qnapstats==0.3.1"], + "requirements": ["qnapstats==0.4.0"], "codeowners": ["@colinodell"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 09a67edb29c..dce813853da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1990,7 +1990,7 @@ pyzbar==0.1.7 pyzerproc==0.4.8 # homeassistant.components.qnap -qnapstats==0.3.1 +qnapstats==0.4.0 # homeassistant.components.quantum_gateway quantum-gateway==0.0.5 From 8d7a136fc4e90b4f0136fc410c447598eea290cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Jur=C4=8Da?= Date: Sat, 14 Aug 2021 11:05:23 +0200 Subject: [PATCH 196/355] Add MySensors S_MOISTURE type as sensor (#54583) --- homeassistant/components/mysensors/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index bb56770fd0c..396d0e2519b 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -85,6 +85,7 @@ SENSORS: dict[str, list[str | None] | dict[str, list[str | None]]] = { DEVICE_CLASS_ILLUMINANCE, STATE_CLASS_MEASUREMENT, ], + "S_MOISTURE": [PERCENTAGE, "mdi:water-percent", None, None], }, "V_VOLTAGE": [ ELECTRIC_POTENTIAL_VOLT, From a9807e5fa792965d010acf5642a5042bc79cec45 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 15 Aug 2021 00:11:00 +0000 Subject: [PATCH 197/355] [ci skip] Translation update --- .../components/sensor/translations/nl.json | 2 ++ .../components/tractive/translations/nl.json | 19 +++++++++++++++++++ .../uptimerobot/translations/nl.json | 17 +++++++++++++++-- .../components/weather/translations/ru.json | 2 +- 4 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/tractive/translations/nl.json diff --git a/homeassistant/components/sensor/translations/nl.json b/homeassistant/components/sensor/translations/nl.json index 745e097c6ee..933caf15de8 100644 --- a/homeassistant/components/sensor/translations/nl.json +++ b/homeassistant/components/sensor/translations/nl.json @@ -6,6 +6,7 @@ "is_carbon_monoxide": "Huidig niveau {entity_name} koolmonoxideconcentratie", "is_current": "Huidige {entity_name} stroom", "is_energy": "Huidige {entity_name} energie", + "is_gas": "Huidig {entity_name} gas", "is_humidity": "Huidige {entity_name} vochtigheidsgraad", "is_illuminance": "Huidige {entity_name} verlichtingssterkte", "is_power": "Huidige {entity_name}\nvermogen", @@ -22,6 +23,7 @@ "carbon_monoxide": "{entity_name} koolmonoxideconcentratie gewijzigd", "current": "{entity_name} huidige wijzigingen", "energy": "{entity_name} energieveranderingen", + "gas": "{entity_name} gas verandert", "humidity": "{entity_name} vochtigheidsgraad gewijzigd", "illuminance": "{entity_name} verlichtingssterkte gewijzigd", "power": "{entity_name} vermogen gewijzigd", diff --git a/homeassistant/components/tractive/translations/nl.json b/homeassistant/components/tractive/translations/nl.json new file mode 100644 index 00000000000..2ae14092cde --- /dev/null +++ b/homeassistant/components/tractive/translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "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/uptimerobot/translations/nl.json b/homeassistant/components/uptimerobot/translations/nl.json index 3a77fedf228..7e0ad6a3cd0 100644 --- a/homeassistant/components/uptimerobot/translations/nl.json +++ b/homeassistant/components/uptimerobot/translations/nl.json @@ -1,17 +1,30 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "reauth_failed_existing": "Kon de config entry niet updaten, gelieve de integratie te verwijderen en het opnieuw op te zetten.", + "reauth_successful": "Herauthenticatie was succesvol", + "unknown": "Onverwachte fout" }, "error": { "cannot_connect": "Kan geen verbinding maken", + "invalid_api_key": "Ongeldige API-sleutel", + "reauth_failed_matching_account": "De API sleutel die u heeft opgegeven komt niet overeen met de account ID voor de bestaande configuratie.", "unknown": "Onverwachte fout" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API-sleutel" + }, + "description": "U moet een alleen-lezen API-sleutel van Uptime Robot opgeven", + "title": "Verifieer de integratie opnieuw" + }, "user": { "data": { "api_key": "API-sleutel" - } + }, + "description": "U moet een alleen-lezen API-sleutel van Uptime Robot opgeven" } } } diff --git a/homeassistant/components/weather/translations/ru.json b/homeassistant/components/weather/translations/ru.json index 1f0458b7653..b0f92257631 100644 --- a/homeassistant/components/weather/translations/ru.json +++ b/homeassistant/components/weather/translations/ru.json @@ -1,7 +1,7 @@ { "state": { "_": { - "clear-night": "\u042f\u0441\u043d\u043e, \u043d\u043e\u0447\u044c", + "clear-night": "\u042f\u0441\u043d\u043e", "cloudy": "\u041e\u0431\u043b\u0430\u0447\u043d\u043e", "exceptional": "\u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435", "fog": "\u0422\u0443\u043c\u0430\u043d", From 87e7a8fb5f5a0108acf0349f195f1c43fe58adda Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 15 Aug 2021 08:51:43 +0200 Subject: [PATCH 198/355] Move temperature conversions to sensor base class - new integrations (#54623) * Move temperature conversions to sensor base class * Tweaks * Update pvpc_hourly_pricing * Fix flipr sensor * Fix ezviz and youless sensor --- homeassistant/components/acmeda/sensor.py | 2 +- .../components/advantage_air/sensor.py | 2 +- homeassistant/components/canary/sensor.py | 2 +- homeassistant/components/ezviz/sensor.py | 5 ++- homeassistant/components/flipr/sensor.py | 8 ++-- homeassistant/components/fritzbox/sensor.py | 4 +- homeassistant/components/fronius/sensor.py | 6 +-- homeassistant/components/mill/sensor.py | 6 +-- homeassistant/components/powerwall/sensor.py | 4 +- .../components/pvpc_hourly_pricing/sensor.py | 9 ++-- homeassistant/components/renault/sensor.py | 42 +++++++++---------- .../components/smartthings/sensor.py | 4 +- homeassistant/components/spider/sensor.py | 8 ++-- homeassistant/components/tplink/sensor.py | 2 +- homeassistant/components/wemo/sensor.py | 4 +- .../components/xiaomi_miio/sensor.py | 2 +- homeassistant/components/youless/sensor.py | 8 ++-- 17 files changed, 60 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/acmeda/sensor.py b/homeassistant/components/acmeda/sensor.py index 7cded0adb30..43f5e32c74f 100644 --- a/homeassistant/components/acmeda/sensor.py +++ b/homeassistant/components/acmeda/sensor.py @@ -34,7 +34,7 @@ class AcmedaBattery(AcmedaBase, SensorEntity): """Representation of a Acmeda cover device.""" device_class = DEVICE_CLASS_BATTERY - unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE @property def name(self): diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index 65b7b35740e..5912101fd65 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -154,6 +154,6 @@ class AdvantageAirZoneTemp(AdvantageAirEntity, SensorEntity): ) @property - def state(self): + def native_value(self): """Return the current value of the measured temperature.""" return self._zone["measuredTemp"] diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 3870cb357ef..1e7747039b8 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -120,7 +120,7 @@ class CanarySensor(CoordinatorEntity, SensorEntity): "model": device.device_type["name"], "manufacturer": MANUFACTURER, } - self._attr_unit_of_measurement = sensor_type[1] + self._attr_native_unit_of_measurement = sensor_type[1] self._attr_device_class = sensor_type[3] self._attr_icon = sensor_type[2] diff --git a/homeassistant/components/ezviz/sensor.py b/homeassistant/components/ezviz/sensor.py index 42283b52d35..512491a2548 100644 --- a/homeassistant/components/ezviz/sensor.py +++ b/homeassistant/components/ezviz/sensor.py @@ -5,9 +5,10 @@ import logging from pyezviz.constants import SensorType +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -39,7 +40,7 @@ async def async_setup_entry( async_add_entities(sensors) -class EzvizSensor(CoordinatorEntity, Entity): +class EzvizSensor(CoordinatorEntity, SensorEntity): """Representation of a Ezviz sensor.""" coordinator: EzvizDataUpdateCoordinator diff --git a/homeassistant/components/flipr/sensor.py b/homeassistant/components/flipr/sensor.py index 427a668a72b..3a02b7e2f2e 100644 --- a/homeassistant/components/flipr/sensor.py +++ b/homeassistant/components/flipr/sensor.py @@ -1,13 +1,13 @@ """Sensor platform for the Flipr's pool_sensor.""" from datetime import datetime +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, TEMP_CELSIUS, ) -from homeassistant.helpers.entity import Entity from . import FliprEntity from .const import ATTRIBUTION, CONF_FLIPR_ID, DOMAIN @@ -53,7 +53,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors_list, True) -class FliprSensor(FliprEntity, Entity): +class FliprSensor(FliprEntity, SensorEntity): """Sensor representing FliprSensor data.""" @property @@ -62,7 +62,7 @@ class FliprSensor(FliprEntity, Entity): return f"Flipr {self.flipr_id} {SENSORS[self.info_type]['name']}" @property - def state(self): + def native_value(self): """State of the sensor.""" state = self.coordinator.data[self.info_type] if isinstance(state, datetime): @@ -80,7 +80,7 @@ class FliprSensor(FliprEntity, Entity): return SENSORS[self.info_type]["icon"] @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return unit of measurement.""" return SENSORS[self.info_type]["unit"] diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 01bea17fb3c..56e025cd605 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -136,7 +136,7 @@ class FritzBoxPowerSensor(FritzBoxSensor): """The entity class for FRITZ!SmartHome power consumption sensors.""" @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the state of the sensor.""" if power := self.device.power: return power / 1000 # type: ignore [no-any-return] @@ -147,7 +147,7 @@ class FritzBoxEnergySensor(FritzBoxSensor): """The entity class for FRITZ!SmartHome total energy sensors.""" @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the state of the sensor.""" if energy := self.device.energy: return energy / 1000 # type: ignore [no-any-return] diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 68430684d85..5141c79f31b 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -306,9 +306,9 @@ class FroniusTemplateSensor(SensorEntity): async def async_update(self): """Update the internal state.""" state = self._parent.data.get(self._key) - self._attr_state = state.get("value") - if isinstance(self._attr_state, float): - self._attr_native_value = round(self._attr_state, 2) + self._attr_native_value = state.get("value") + if isinstance(self._attr_native_value, float): + self._attr_native_value = round(self._attr_native_value, 2) self._attr_native_unit_of_measurement = state.get("unit") @property diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index bdd1a90fb38..a8b4554139f 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -36,7 +36,7 @@ class MillHeaterEnergySensor(SensorEntity): self._attr_device_class = DEVICE_CLASS_ENERGY self._attr_name = f"{heater.name} {sensor_type.replace('_', ' ')}" self._attr_unique_id = f"{heater.device_id}_{sensor_type}" - self._attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR + self._attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR self._attr_state_class = STATE_CLASS_MEASUREMENT self._attr_device_info = { "identifiers": {(DOMAIN, heater.device_id)}, @@ -67,7 +67,7 @@ class MillHeaterEnergySensor(SensorEntity): else: _state = None if _state is None: - self._attr_state = _state + self._attr_native_value = _state return if self.state is not None and _state < self.state: @@ -81,4 +81,4 @@ class MillHeaterEnergySensor(SensorEntity): month=1, day=1, hour=0, minute=0, second=0, microsecond=0 ) ) - self._attr_state = _state + self._attr_native_value = _state diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index 96542dc3929..0ffa333181d 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -152,7 +152,7 @@ class PowerWallEnergyDirectionSensor(PowerWallEntity, SensorEntity): """Representation of an Powerwall Direction Energy sensor.""" _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR + _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR _attr_device_class = DEVICE_CLASS_ENERGY _attr_last_reset = dt_util.utc_from_timestamp(0) @@ -180,7 +180,7 @@ class PowerWallEnergyDirectionSensor(PowerWallEntity, SensorEntity): ) @property - def state(self): + def native_value(self): """Get the current value in kWh.""" meter = self.coordinator.data[POWERWALL_API_METERS].get_meter(self._meter) if self._meter_direction == _METER_DIRECTION_EXPORT: diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py index 157327fbd19..9cc5603e35b 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 SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CURRENCY_EURO, ENERGY_KILO_WATT_HOUR from homeassistant.core import HomeAssistant, callback @@ -51,9 +51,10 @@ async def async_setup_entry( class ElecPriceSensor(RestoreEntity, SensorEntity): """Class to hold the prices of electricity as a sensor.""" - unit_of_measurement = UNIT - icon = ICON - should_poll = False + _attr_icon = ICON + _attr_native_unit_of_measurement = UNIT + _attr_should_poll = False + _attr_state_class = STATE_CLASS_MEASUREMENT def __init__(self, name, unique_id, pvpc_data_handler): """Initialize the sensor object.""" diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index 8403a04d001..51f38d6a4d6 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -88,10 +88,10 @@ class RenaultBatteryAutonomySensor(RenaultBatteryDataEntity, SensorEntity): """Battery autonomy sensor.""" _attr_icon = "mdi:ev-station" - _attr_unit_of_measurement = LENGTH_KILOMETERS + _attr_native_unit_of_measurement = LENGTH_KILOMETERS @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of this entity.""" return self.data.batteryAutonomy if self.data else None @@ -100,10 +100,10 @@ class RenaultBatteryLevelSensor(RenaultBatteryDataEntity, SensorEntity): """Battery Level sensor.""" _attr_device_class = DEVICE_CLASS_BATTERY - _attr_unit_of_measurement = PERCENTAGE + _attr_native_unit_of_measurement = PERCENTAGE @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of this entity.""" return self.data.batteryLevel if self.data else None @@ -128,10 +128,10 @@ class RenaultBatteryTemperatureSensor(RenaultBatteryDataEntity, SensorEntity): """Battery Temperature sensor.""" _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_unit_of_measurement = TEMP_CELSIUS + _attr_native_unit_of_measurement = TEMP_CELSIUS @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of this entity.""" return self.data.batteryTemperature if self.data else None @@ -142,7 +142,7 @@ class RenaultChargeModeSensor(RenaultChargeModeDataEntity, SensorEntity): _attr_device_class = DEVICE_CLASS_CHARGE_MODE @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of this entity.""" return self.data.chargeMode if self.data else None @@ -160,7 +160,7 @@ class RenaultChargeStateSensor(RenaultBatteryDataEntity, SensorEntity): _attr_device_class = DEVICE_CLASS_CHARGE_STATE @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of this entity.""" charging_status = self.data.get_charging_status() if self.data else None return slugify(charging_status.name) if charging_status is not None else None @@ -175,10 +175,10 @@ class RenaultChargingRemainingTimeSensor(RenaultBatteryDataEntity, SensorEntity) """Charging Remaining Time sensor.""" _attr_icon = "mdi:timer" - _attr_unit_of_measurement = TIME_MINUTES + _attr_native_unit_of_measurement = TIME_MINUTES @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of this entity.""" return self.data.chargingRemainingTime if self.data else None @@ -187,10 +187,10 @@ class RenaultChargingPowerSensor(RenaultBatteryDataEntity, SensorEntity): """Charging Power sensor.""" _attr_device_class = DEVICE_CLASS_ENERGY - _attr_unit_of_measurement = POWER_KILO_WATT + _attr_native_unit_of_measurement = POWER_KILO_WATT @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the state of this entity.""" if not self.data or self.data.chargingInstantaneousPower is None: return None @@ -204,10 +204,10 @@ class RenaultFuelAutonomySensor(RenaultCockpitDataEntity, SensorEntity): """Fuel autonomy sensor.""" _attr_icon = "mdi:gas-station" - _attr_unit_of_measurement = LENGTH_KILOMETERS + _attr_native_unit_of_measurement = LENGTH_KILOMETERS @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of this entity.""" return ( round(self.data.fuelAutonomy) @@ -220,10 +220,10 @@ class RenaultFuelQuantitySensor(RenaultCockpitDataEntity, SensorEntity): """Fuel quantity sensor.""" _attr_icon = "mdi:fuel" - _attr_unit_of_measurement = VOLUME_LITERS + _attr_native_unit_of_measurement = VOLUME_LITERS @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of this entity.""" return ( round(self.data.fuelQuantity) @@ -236,10 +236,10 @@ class RenaultMileageSensor(RenaultCockpitDataEntity, SensorEntity): """Mileage sensor.""" _attr_icon = "mdi:sign-direction" - _attr_unit_of_measurement = LENGTH_KILOMETERS + _attr_native_unit_of_measurement = LENGTH_KILOMETERS @property - def state(self) -> int | None: + def native_value(self) -> int | None: """Return the state of this entity.""" return ( round(self.data.totalMileage) @@ -252,10 +252,10 @@ class RenaultOutsideTemperatureSensor(RenaultHVACDataEntity, SensorEntity): """HVAC Outside Temperature sensor.""" _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_unit_of_measurement = TEMP_CELSIUS + _attr_native_unit_of_measurement = TEMP_CELSIUS @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the state of this entity.""" return self.data.externalTemperature if self.data else None @@ -266,7 +266,7 @@ class RenaultPlugStateSensor(RenaultBatteryDataEntity, SensorEntity): _attr_device_class = DEVICE_CLASS_PLUG_STATE @property - def state(self) -> str | None: + def native_value(self) -> str | None: """Return the state of this entity.""" plug_status = self.data.get_plug_status() if self.data else None return slugify(plug_status.name) if plug_status is not None else None diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index c4d84ec5f69..a8e6c0472e9 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -568,7 +568,7 @@ class SmartThingsPowerConsumptionSensor(SmartThingsEntity, SensorEntity): return f"{self._device.device_id}.{self.report_name}" @property - def state(self): + def native_value(self): """Return the state of the sensor.""" value = self._device.status.attributes[Attribute.power_consumption].value if value is None or value.get(self.report_name) is None: @@ -585,7 +585,7 @@ class SmartThingsPowerConsumptionSensor(SmartThingsEntity, SensorEntity): return DEVICE_CLASS_ENERGY @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit this state is expressed in.""" if self.report_name == "power": return POWER_WATT diff --git a/homeassistant/components/spider/sensor.py b/homeassistant/components/spider/sensor.py index 998a9ff8eee..bf1ab0b18be 100644 --- a/homeassistant/components/spider/sensor.py +++ b/homeassistant/components/spider/sensor.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass, config, async_add_entities): class SpiderPowerPlugEnergy(SensorEntity): """Representation of a Spider Power Plug (energy).""" - _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR + _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR _attr_device_class = DEVICE_CLASS_ENERGY _attr_state_class = STATE_CLASS_MEASUREMENT @@ -59,7 +59,7 @@ class SpiderPowerPlugEnergy(SensorEntity): return f"{self.power_plug.name} Total Energy Today" @property - def state(self) -> float: + def native_value(self) -> float: """Return todays energy usage in Kwh.""" return round(self.power_plug.today_energy_consumption / 1000, 2) @@ -80,7 +80,7 @@ class SpiderPowerPlugPower(SensorEntity): _attr_device_class = DEVICE_CLASS_POWER _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_unit_of_measurement = POWER_WATT + _attr_native_unit_of_measurement = POWER_WATT def __init__(self, api, power_plug) -> None: """Initialize the Spider Power Plug.""" @@ -108,7 +108,7 @@ class SpiderPowerPlugPower(SensorEntity): return f"{self.power_plug.name} Power Consumption" @property - def state(self) -> float: + def native_value(self) -> float: """Return the current power usage in W.""" return round(self.power_plug.current_energy_consumption) diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index 697641915f7..fae7939cd65 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -137,7 +137,7 @@ class SmartPlugSensor(CoordinatorEntity, SensorEntity): return self.coordinator.data @property - def state(self) -> float | None: + def native_value(self) -> float | None: """Return the sensors state.""" return self.data[CONF_EMETER_PARAMS][self.entity_description.key] diff --git a/homeassistant/components/wemo/sensor.py b/homeassistant/components/wemo/sensor.py index ebd68231e0c..b9d22e6995a 100644 --- a/homeassistant/components/wemo/sensor.py +++ b/homeassistant/components/wemo/sensor.py @@ -98,7 +98,7 @@ class InsightCurrentPower(InsightSensor): ) @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the current power consumption.""" return ( convert(self.wemo.insight_params[self.entity_description.key], float, 0.0) @@ -123,7 +123,7 @@ class InsightTodayEnergy(InsightSensor): return dt.start_of_local_day() @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Return the current energy use today.""" miliwatts = convert( self.wemo.insight_params[self.entity_description.key], float, 0.0 diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index bbf83825dca..aff0b5212f1 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -280,7 +280,7 @@ class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity): self.entity_description = description @property - def state(self): + def native_value(self): """Return the state of the device.""" self._state = self._extract_value_from_attribute( self.coordinator.data, self.entity_description.key diff --git a/homeassistant/components/youless/sensor.py b/homeassistant/components/youless/sensor.py index 54155034919..bc0f1ee873b 100644 --- a/homeassistant/components/youless/sensor.py +++ b/homeassistant/components/youless/sensor.py @@ -3,11 +3,11 @@ from __future__ import annotations from youless_api.youless_sensor import YoulessSensor +from homeassistant.components.sensor import SensorEntity from homeassistant.components.youless import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, DEVICE_CLASS_POWER from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( @@ -40,7 +40,7 @@ async def async_setup_entry( ) -class YoulessBaseSensor(CoordinatorEntity, Entity): +class YoulessBaseSensor(CoordinatorEntity, SensorEntity): """The base sensor for Youless.""" def __init__( @@ -71,7 +71,7 @@ class YoulessBaseSensor(CoordinatorEntity, Entity): return None @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement for the sensor.""" if self.get_sensor is None: return None @@ -79,7 +79,7 @@ class YoulessBaseSensor(CoordinatorEntity, Entity): return self.get_sensor.unit_of_measurement @property - def state(self) -> StateType: + def native_value(self) -> StateType: """Determine the state value, only if a sensor is initialized.""" if self.get_sensor is None: return None From 675441142d1fe51b0216365810b298c15704f9ca Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Sun, 15 Aug 2021 12:50:40 +0200 Subject: [PATCH 199/355] Update pyhomematic to 0.1.74 (#54613) --- homeassistant/components/homematic/const.py | 2 ++ homeassistant/components/homematic/manifest.json | 2 +- homeassistant/components/homematic/sensor.py | 4 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py index 4f1c1d12f81..0880d168375 100644 --- a/homeassistant/components/homematic/const.py +++ b/homeassistant/components/homematic/const.py @@ -62,6 +62,7 @@ HM_DEVICE_TYPES = { "IPWIODevice", "IPSwitchBattery", "IPMultiIOPCB", + "IPGarageSwitch", ], DISCOVER_LIGHTS: [ "Dimmer", @@ -125,6 +126,7 @@ HM_DEVICE_TYPES = { "TempModuleSTE2", "IPMultiIOPCB", "ValveBoxW", + "CO2SensorIP", ], DISCOVER_CLIMATE: [ "Thermostat", diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index 8b1ee62a09e..f500ef54b56 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -2,7 +2,7 @@ "domain": "homematic", "name": "Homematic", "documentation": "https://www.home-assistant.io/integrations/homematic", - "requirements": ["pyhomematic==0.1.73"], + "requirements": ["pyhomematic==0.1.74"], "codeowners": ["@pvizeli", "@danielperna84"], "iot_class": "local_push" } diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 7cfe0ffc944..18690ac3553 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -3,7 +3,9 @@ import logging from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( + CONCENTRATION_PARTS_PER_MILLION, DEGREE, + DEVICE_CLASS_CO2, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, @@ -72,6 +74,7 @@ HM_UNIT_HA_CAST = { "VALVE_STATE": PERCENTAGE, "CARRIER_SENSE_LEVEL": PERCENTAGE, "DUTY_CYCLE_LEVEL": PERCENTAGE, + "CONCENTRATION": CONCENTRATION_PARTS_PER_MILLION, } HM_DEVICE_CLASS_HA_CAST = { @@ -85,6 +88,7 @@ HM_DEVICE_CLASS_HA_CAST = { "HIGHEST_ILLUMINATION": DEVICE_CLASS_ILLUMINANCE, "POWER": DEVICE_CLASS_POWER, "CURRENT": DEVICE_CLASS_POWER, + "CONCENTRATION": DEVICE_CLASS_CO2, } HM_ICON_HA_CAST = {"WIND_SPEED": "mdi:weather-windy", "BRIGHTNESS": "mdi:invert-colors"} diff --git a/requirements_all.txt b/requirements_all.txt index dce813853da..c6b8d084c8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1487,7 +1487,7 @@ pyhik==0.2.8 pyhiveapi==0.4.2 # homeassistant.components.homematic -pyhomematic==0.1.73 +pyhomematic==0.1.74 # homeassistant.components.homeworks pyhomeworks==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 44ab676e9f4..cf73f4538cd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -839,7 +839,7 @@ pyheos==0.7.2 pyhiveapi==0.4.2 # homeassistant.components.homematic -pyhomematic==0.1.73 +pyhomematic==0.1.74 # homeassistant.components.ialarm pyialarm==1.9.0 From 5ed9cd7153ebb8bd96dd26a8d40fed0a8e992b7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sun, 15 Aug 2021 13:16:10 +0200 Subject: [PATCH 200/355] Adax, update requirements (#54587) 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/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/adax/manifest.json b/homeassistant/components/adax/manifest.json index 36106290ed6..3d2c9273d05 100644 --- a/homeassistant/components/adax/manifest.json +++ b/homeassistant/components/adax/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/adax", "requirements": [ - "adax==0.0.1" + "adax==0.1.1" ], "codeowners": [ "@danielhiversen" diff --git a/requirements_all.txt b/requirements_all.txt index c6b8d084c8e..32f92439be8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -106,7 +106,7 @@ adafruit-circuitpython-dht==3.6.0 adafruit-circuitpython-mcp230xx==2.2.2 # homeassistant.components.adax -adax==0.0.1 +adax==0.1.1 # homeassistant.components.androidtv adb-shell[async]==0.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf73f4538cd..315ee031f03 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -48,7 +48,7 @@ abodepy==1.2.0 accuweather==0.2.0 # homeassistant.components.adax -adax==0.0.1 +adax==0.1.1 # homeassistant.components.androidtv adb-shell[async]==0.3.4 From ebaae8d2bf9982ab78eb92cb9699338587214738 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 15 Aug 2021 13:49:29 +0200 Subject: [PATCH 201/355] Add sensor platform for Xiaomi Miio fans (#54564) --- .../components/xiaomi_miio/__init__.py | 9 +- homeassistant/components/xiaomi_miio/const.py | 4 +- homeassistant/components/xiaomi_miio/fan.py | 70 ------- .../components/xiaomi_miio/sensor.py | 193 +++++++++++++++--- 4 files changed, 174 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index faff2194948..122c42c6589 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -31,7 +31,6 @@ from .const import ( KEY_DEVICE, MODELS_AIR_MONITOR, MODELS_FAN, - MODELS_FAN_MIIO, MODELS_HUMIDIFIER, MODELS_HUMIDIFIER_MIIO, MODELS_HUMIDIFIER_MIOT, @@ -47,7 +46,7 @@ _LOGGER = logging.getLogger(__name__) GATEWAY_PLATFORMS = ["alarm_control_panel", "light", "sensor", "switch"] SWITCH_PLATFORMS = ["switch"] -FAN_PLATFORMS = ["fan"] +FAN_PLATFORMS = ["fan", "sensor"] HUMIDIFIER_PLATFORMS = [ "binary_sensor", "humidifier", @@ -120,11 +119,7 @@ async def async_create_miio_device_and_coordinator( device = None migrate = False - if ( - model not in MODELS_HUMIDIFIER - and model not in MODELS_PURIFIER_MIOT - and model not in MODELS_FAN_MIIO - ): + if model not in MODELS_HUMIDIFIER and model not in MODELS_FAN: return _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index c407e92a6ae..de1c0bcf007 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -63,7 +63,7 @@ MODELS_PURIFIER_MIOT = [ MODEL_AIRPURIFIER_3H, MODEL_AIRPURIFIER_PROH, ] -MODELS_FAN_MIIO = [ +MODELS_PURIFIER_MIIO = [ MODEL_AIRPURIFIER_V1, MODEL_AIRPURIFIER_V2, MODEL_AIRPURIFIER_V3, @@ -124,7 +124,7 @@ MODELS_SWITCH = [ "chuangmi.plug.hmi205", "chuangmi.plug.hmi206", ] -MODELS_FAN = MODELS_FAN_MIIO + MODELS_PURIFIER_MIOT +MODELS_FAN = MODELS_PURIFIER_MIIO + MODELS_PURIFIER_MIOT MODELS_HUMIDIFIER = ( MODELS_HUMIDIFIER_MIOT + MODELS_HUMIDIFIER_MIIO + MODELS_HUMIDIFIER_MJJSQ ) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index fe4df2cd6d3..5b3418c83f5 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -28,7 +28,6 @@ from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, - ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, CONF_TOKEN, @@ -103,26 +102,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ATTR_MODEL = "model" # Air Purifier -ATTR_HUMIDITY = "humidity" -ATTR_AIR_QUALITY_INDEX = "aqi" -ATTR_FILTER_HOURS_USED = "filter_hours_used" ATTR_FILTER_LIFE = "filter_life_remaining" ATTR_FAVORITE_LEVEL = "favorite_level" ATTR_BUZZER = "buzzer" ATTR_CHILD_LOCK = "child_lock" ATTR_LED = "led" ATTR_LED_BRIGHTNESS = "led_brightness" -ATTR_MOTOR_SPEED = "motor_speed" -ATTR_AVERAGE_AIR_QUALITY_INDEX = "average_aqi" -ATTR_PURIFY_VOLUME = "purify_volume" ATTR_BRIGHTNESS = "brightness" ATTR_LEVEL = "level" ATTR_FAN_LEVEL = "fan_level" -ATTR_MOTOR2_SPEED = "motor2_speed" -ATTR_ILLUMINANCE = "illuminance" -ATTR_FILTER_RFID_PRODUCT_ID = "filter_rfid_product_id" -ATTR_FILTER_RFID_TAG = "filter_rfid_tag" -ATTR_FILTER_TYPE = "filter_type" ATTR_LEARN_MODE = "learn_mode" ATTR_SLEEP_TIME = "sleep_time" ATTR_SLEEP_LEARN_COUNT = "sleep_mode_learn_count" @@ -135,22 +123,12 @@ ATTR_VOLUME = "volume" ATTR_USE_TIME = "use_time" ATTR_BUTTON_PRESSED = "button_pressed" -# Air Fresh -ATTR_CO2 = "co2" - # Map attributes to properties of the state object AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = { - ATTR_TEMPERATURE: "temperature", - ATTR_HUMIDITY: "humidity", - ATTR_AIR_QUALITY_INDEX: "aqi", ATTR_MODE: "mode", - ATTR_FILTER_HOURS_USED: "filter_hours_used", - ATTR_FILTER_LIFE: "filter_life_remaining", ATTR_FAVORITE_LEVEL: "favorite_level", ATTR_CHILD_LOCK: "child_lock", ATTR_LED: "led", - ATTR_MOTOR_SPEED: "motor_speed", - ATTR_AVERAGE_AIR_QUALITY_INDEX: "average_aqi", ATTR_LEARN_MODE: "learn_mode", ATTR_EXTRA_FEATURES: "extra_features", ATTR_TURBO_MODE_SUPPORTED: "turbo_mode_supported", @@ -159,7 +137,6 @@ AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = { AVAILABLE_ATTRIBUTES_AIRPURIFIER = { **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON, - ATTR_PURIFY_VOLUME: "purify_volume", ATTR_SLEEP_TIME: "sleep_time", ATTR_SLEEP_LEARN_COUNT: "sleep_mode_learn_count", ATTR_AUTO_DETECT: "auto_detect", @@ -171,15 +148,8 @@ AVAILABLE_ATTRIBUTES_AIRPURIFIER = { AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO = { **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON, - ATTR_PURIFY_VOLUME: "purify_volume", ATTR_USE_TIME: "use_time", - ATTR_FILTER_RFID_PRODUCT_ID: "filter_rfid_product_id", - ATTR_FILTER_RFID_TAG: "filter_rfid_tag", - ATTR_FILTER_TYPE: "filter_type", - ATTR_ILLUMINANCE: "illuminance", - ATTR_MOTOR2_SPEED: "motor2_speed", ATTR_VOLUME: "volume", - # perhaps supported but unconfirmed ATTR_AUTO_DETECT: "auto_detect", ATTR_SLEEP_TIME: "sleep_time", ATTR_SLEEP_LEARN_COUNT: "sleep_mode_learn_count", @@ -187,64 +157,32 @@ AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO = { AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7 = { **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON, - ATTR_FILTER_RFID_PRODUCT_ID: "filter_rfid_product_id", - ATTR_FILTER_RFID_TAG: "filter_rfid_tag", - ATTR_FILTER_TYPE: "filter_type", - ATTR_ILLUMINANCE: "illuminance", - ATTR_MOTOR2_SPEED: "motor2_speed", ATTR_VOLUME: "volume", } AVAILABLE_ATTRIBUTES_AIRPURIFIER_2S = { **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON, ATTR_BUZZER: "buzzer", - ATTR_FILTER_RFID_PRODUCT_ID: "filter_rfid_product_id", - ATTR_FILTER_RFID_TAG: "filter_rfid_tag", - ATTR_FILTER_TYPE: "filter_type", - ATTR_ILLUMINANCE: "illuminance", } AVAILABLE_ATTRIBUTES_AIRPURIFIER_3 = { - ATTR_TEMPERATURE: "temperature", - ATTR_HUMIDITY: "humidity", - ATTR_AIR_QUALITY_INDEX: "aqi", ATTR_MODE: "mode", - ATTR_FILTER_HOURS_USED: "filter_hours_used", - ATTR_FILTER_LIFE: "filter_life_remaining", ATTR_FAVORITE_LEVEL: "favorite_level", ATTR_CHILD_LOCK: "child_lock", ATTR_LED: "led", - ATTR_MOTOR_SPEED: "motor_speed", - ATTR_AVERAGE_AIR_QUALITY_INDEX: "average_aqi", - ATTR_PURIFY_VOLUME: "purify_volume", ATTR_USE_TIME: "use_time", ATTR_BUZZER: "buzzer", ATTR_LED_BRIGHTNESS: "led_brightness", - ATTR_FILTER_RFID_PRODUCT_ID: "filter_rfid_product_id", - ATTR_FILTER_RFID_TAG: "filter_rfid_tag", - ATTR_FILTER_TYPE: "filter_type", ATTR_FAN_LEVEL: "fan_level", } AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 = { # Common set isn't used here. It's a very basic version of the device. - ATTR_AIR_QUALITY_INDEX: "aqi", ATTR_MODE: "mode", ATTR_LED: "led", ATTR_BUZZER: "buzzer", ATTR_CHILD_LOCK: "child_lock", - ATTR_ILLUMINANCE: "illuminance", - ATTR_FILTER_HOURS_USED: "filter_hours_used", - ATTR_FILTER_LIFE: "filter_life_remaining", - ATTR_MOTOR_SPEED: "motor_speed", - # perhaps supported but unconfirmed - ATTR_AVERAGE_AIR_QUALITY_INDEX: "average_aqi", ATTR_VOLUME: "volume", - ATTR_MOTOR2_SPEED: "motor2_speed", - ATTR_FILTER_RFID_PRODUCT_ID: "filter_rfid_product_id", - ATTR_FILTER_RFID_TAG: "filter_rfid_tag", - ATTR_FILTER_TYPE: "filter_type", - ATTR_PURIFY_VOLUME: "purify_volume", ATTR_LEARN_MODE: "learn_mode", ATTR_SLEEP_TIME: "sleep_time", ATTR_SLEEP_LEARN_COUNT: "sleep_mode_learn_count", @@ -255,20 +193,12 @@ AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 = { } AVAILABLE_ATTRIBUTES_AIRFRESH = { - ATTR_TEMPERATURE: "temperature", - ATTR_AIR_QUALITY_INDEX: "aqi", - ATTR_AVERAGE_AIR_QUALITY_INDEX: "average_aqi", - ATTR_CO2: "co2", - ATTR_HUMIDITY: "humidity", ATTR_MODE: "mode", ATTR_LED: "led", ATTR_LED_BRIGHTNESS: "led_brightness", ATTR_BUZZER: "buzzer", ATTR_CHILD_LOCK: "child_lock", - ATTR_FILTER_LIFE: "filter_life_remaining", - ATTR_FILTER_HOURS_USED: "filter_hours_used", ATTR_USE_TIME: "use_time", - ATTR_MOTOR_SPEED: "motor_speed", ATTR_EXTRA_FEATURES: "extra_features", } diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index aff0b5212f1..26914e1dff8 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -19,6 +19,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, ) @@ -26,18 +27,25 @@ from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, CONF_HOST, CONF_NAME, CONF_TOKEN, + DEVICE_CLASS_CO2, + DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + LIGHT_LUX, PERCENTAGE, POWER_WATT, PRESSURE_HPA, TEMP_CELSIUS, + TIME_HOURS, + VOLUME_CUBIC_METERS, ) import homeassistant.helpers.config_validation as cv @@ -49,11 +57,18 @@ from .const import ( DOMAIN, KEY_COORDINATOR, KEY_DEVICE, + MODEL_AIRFRESH_VA2, MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1, + MODEL_AIRPURIFIER_PRO, + MODEL_AIRPURIFIER_PRO_V7, + MODEL_AIRPURIFIER_V2, + MODEL_AIRPURIFIER_V3, MODELS_HUMIDIFIER_MIIO, MODELS_HUMIDIFIER_MIOT, MODELS_HUMIDIFIER_MJJSQ, + MODELS_PURIFIER_MIIO, + MODELS_PURIFIER_MIOT, ) from .device import XiaomiCoordinatedMiioEntity, XiaomiMiioEntity from .gateway import XiaomiGatewayDevice @@ -73,17 +88,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ATTR_ACTUAL_SPEED = "actual_speed" ATTR_AIR_QUALITY = "air_quality" +ATTR_AQI = "aqi" +ATTR_CARBON_DIOXIDE = "co2" ATTR_CHARGING = "charging" ATTR_DISPLAY_CLOCK = "display_clock" +ATTR_FILTER_LIFE_REMAINING = "filter_life_remaining" +ATTR_FILTER_HOURS_USED = "filter_hours_used" +ATTR_FILTER_USE = "filter_use" ATTR_HUMIDITY = "humidity" ATTR_ILLUMINANCE = "illuminance" +ATTR_ILLUMINANCE_LUX = "illuminance_lux" ATTR_LOAD_POWER = "load_power" +ATTR_MOTOR2_SPEED = "motor2_speed" ATTR_MOTOR_SPEED = "motor_speed" ATTR_NIGHT_MODE = "night_mode" ATTR_NIGHT_TIME_BEGIN = "night_time_begin" ATTR_NIGHT_TIME_END = "night_time_end" +ATTR_PM25 = "pm25" ATTR_POWER = "power" ATTR_PRESSURE = "pressure" +ATTR_PURIFY_VOLUME = "purify_volume" ATTR_SENSOR_STATE = "sensor_state" ATTR_WATER_LEVEL = "water_level" @@ -92,8 +116,7 @@ ATTR_WATER_LEVEL = "water_level" class XiaomiMiioSensorDescription(SensorEntityDescription): """Class that holds device specific info for a xiaomi aqara or humidifier sensor.""" - valid_min_value: float | None = None - valid_max_value: float | None = None + attributes: tuple = () SENSOR_TYPES = { @@ -130,8 +153,6 @@ SENSOR_TYPES = { native_unit_of_measurement=PERCENTAGE, icon="mdi:water-check", state_class=STATE_CLASS_MEASUREMENT, - valid_min_value=0.0, - valid_max_value=100.0, ), ATTR_ACTUAL_SPEED: XiaomiMiioSensorDescription( key=ATTR_ACTUAL_SPEED, @@ -147,6 +168,13 @@ SENSOR_TYPES = { icon="mdi:fast-forward", state_class=STATE_CLASS_MEASUREMENT, ), + 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, + ), ATTR_ILLUMINANCE: XiaomiMiioSensorDescription( key=ATTR_ILLUMINANCE, name="Illuminance", @@ -154,24 +182,144 @@ SENSOR_TYPES = { device_class=DEVICE_CLASS_ILLUMINANCE, state_class=STATE_CLASS_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, + ), ATTR_AIR_QUALITY: XiaomiMiioSensorDescription( key=ATTR_AIR_QUALITY, native_unit_of_measurement="AQI", icon="mdi:cloud", state_class=STATE_CLASS_MEASUREMENT, ), + ATTR_PM25: XiaomiMiioSensorDescription( + key=ATTR_AQI, + name="PM2.5", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + icon="mdi:blur", + state_class=STATE_CLASS_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, + attributes=("filter_type",), + ), + 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, + ), + 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, + ), + 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, + entity_registry_enabled_default=False, + ), } HUMIDIFIER_MIIO_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE) HUMIDIFIER_CA1_CB1_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE, ATTR_MOTOR_SPEED) HUMIDIFIER_MIOT_SENSORS = ( + ATTR_ACTUAL_SPEED, ATTR_HUMIDITY, ATTR_TEMPERATURE, ATTR_WATER_LEVEL, - ATTR_ACTUAL_SPEED, ) HUMIDIFIER_MJJSQ_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE) +PURIFIER_MIIO_SENSORS = ( + ATTR_FILTER_LIFE_REMAINING, + ATTR_FILTER_USE, + ATTR_HUMIDITY, + ATTR_MOTOR_SPEED, + ATTR_PM25, + ATTR_TEMPERATURE, +) +PURIFIER_MIOT_SENSORS = ( + ATTR_FILTER_LIFE_REMAINING, + ATTR_FILTER_USE, + ATTR_HUMIDITY, + ATTR_MOTOR_SPEED, + ATTR_PM25, + ATTR_PURIFY_VOLUME, + ATTR_TEMPERATURE, +) +PURIFIER_V2_SENSORS = ( + ATTR_FILTER_LIFE_REMAINING, + ATTR_FILTER_USE, + ATTR_HUMIDITY, + ATTR_MOTOR_SPEED, + ATTR_PM25, + ATTR_PURIFY_VOLUME, + ATTR_TEMPERATURE, +) +PURIFIER_V3_SENSORS = ( + ATTR_FILTER_LIFE_REMAINING, + ATTR_FILTER_USE, + ATTR_ILLUMINANCE_LUX, + ATTR_MOTOR2_SPEED, + ATTR_MOTOR_SPEED, + ATTR_PM25, + ATTR_PURIFY_VOLUME, +) +PURIFIER_PRO_SENSORS = ( + ATTR_FILTER_LIFE_REMAINING, + ATTR_FILTER_USE, + ATTR_HUMIDITY, + ATTR_ILLUMINANCE_LUX, + ATTR_MOTOR2_SPEED, + ATTR_MOTOR_SPEED, + ATTR_PM25, + ATTR_PURIFY_VOLUME, + ATTR_TEMPERATURE, +) +PURIFIER_PRO_V7_SENSORS = ( + ATTR_FILTER_LIFE_REMAINING, + ATTR_FILTER_USE, + ATTR_HUMIDITY, + ATTR_ILLUMINANCE_LUX, + ATTR_MOTOR2_SPEED, + ATTR_MOTOR_SPEED, + ATTR_PM25, + ATTR_TEMPERATURE, +) +AIRFRESH_SENSORS = ( + ATTR_CARBON_DIOXIDE, + ATTR_FILTER_LIFE_REMAINING, + ATTR_FILTER_USE, + ATTR_HUMIDITY, + ATTR_ILLUMINANCE_LUX, + ATTR_PM25, + ATTR_TEMPERATURE, +) + +MODEL_TO_SENSORS_MAP = { + MODEL_AIRHUMIDIFIER_CA1: HUMIDIFIER_CA1_CB1_SENSORS, + MODEL_AIRHUMIDIFIER_CB1: HUMIDIFIER_CA1_CB1_SENSORS, + MODEL_AIRPURIFIER_V2: PURIFIER_V2_SENSORS, + MODEL_AIRPURIFIER_V3: PURIFIER_V3_SENSORS, + MODEL_AIRPURIFIER_PRO_V7: PURIFIER_PRO_V7_SENSORS, + MODEL_AIRPURIFIER_PRO: PURIFIER_PRO_SENSORS, + MODEL_AIRFRESH_VA2: AIRFRESH_SENSORS, +} + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Import Miio configuration from YAML.""" @@ -224,20 +372,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities): host = config_entry.data[CONF_HOST] token = config_entry.data[CONF_TOKEN] model = config_entry.data[CONF_MODEL] - device = None + device = hass.data[DOMAIN][config_entry.entry_id].get(KEY_DEVICE) sensors = [] - if model in (MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1): - device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] - sensors = HUMIDIFIER_CA1_CB1_SENSORS + if model in MODEL_TO_SENSORS_MAP: + sensors = MODEL_TO_SENSORS_MAP[model] elif model in MODELS_HUMIDIFIER_MIOT: - device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] sensors = HUMIDIFIER_MIOT_SENSORS elif model in MODELS_HUMIDIFIER_MJJSQ: - device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] sensors = HUMIDIFIER_MJJSQ_SENSORS elif model in MODELS_HUMIDIFIER_MIIO: - device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] sensors = HUMIDIFIER_MIIO_SENSORS + elif model in MODELS_PURIFIER_MIIO: + sensors = PURIFIER_MIIO_SENSORS + elif model in MODELS_PURIFIER_MIOT: + sensors = PURIFIER_MIOT_SENSORS else: unique_id = config_entry.unique_id name = config_entry.title @@ -276,24 +424,23 @@ class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity): self._attr_name = name self._attr_unique_id = unique_id - self._state = None self.entity_description = description @property def native_value(self): """Return the state of the device.""" - self._state = self._extract_value_from_attribute( + return self._extract_value_from_attribute( self.coordinator.data, self.entity_description.key ) - if ( - self.entity_description.valid_min_value - and self._state < self.entity_description.valid_min_value - ) or ( - self.entity_description.valid_max_value - and self._state > self.entity_description.valid_max_value - ): - return None - return self._state + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return { + attr: self._extract_value_from_attribute(self.coordinator.data, attr) + for attr in self.entity_description.attributes + if hasattr(self.coordinator.data, attr) + } @staticmethod def _extract_value_from_attribute(state, attribute): From 54da4245079bae53301726a3412d3de9ac38a5a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Aug 2021 08:04:47 -0500 Subject: [PATCH 202/355] Add new OUIs to august for yale branded connect bridges (#54637) --- homeassistant/components/august/manifest.json | 4 ++++ homeassistant/generated/dhcp.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 74caa4b4a78..fc365102926 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -13,6 +13,10 @@ "hostname": "connect", "macaddress": "B8B7F1*" }, + { + "hostname": "connect", + "macaddress": "2C9FFB*" + }, { "hostname": "august*", "macaddress": "E076D0*" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index dbdaaf6da5e..d6b4fc4e457 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -16,6 +16,11 @@ DHCP = [ "hostname": "connect", "macaddress": "B8B7F1*" }, + { + "domain": "august", + "hostname": "connect", + "macaddress": "2C9FFB*" + }, { "domain": "august", "hostname": "august*", From ee7116d0e85ab24548607d6b970d9915f3e3ae0b Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 15 Aug 2021 06:09:06 -0700 Subject: [PATCH 203/355] Treat temporary errors as warnings for Tesla (#54515) * Treat temporary errors as warnings for Tesla closes #53391 * Apply suggestions from code review Co-authored-by: J. Nick Koston * Black Co-authored-by: J. Nick Koston --- homeassistant/components/tesla/__init__.py | 9 +++++++++ homeassistant/components/tesla/config_flow.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index d945d87243e..798e769dc47 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -177,6 +177,15 @@ async def async_setup_entry(hass, config_entry): await async_client.aclose() if ex.code == HTTP_UNAUTHORIZED: raise ConfigEntryAuthFailed from ex + if ex.message in [ + "VEHICLE_UNAVAILABLE", + "TOO_MANY_REQUESTS", + "SERVICE_MAINTENANCE", + "UPSTREAM_TIMEOUT", + ]: + raise ConfigEntryNotReady( + f"Temporarily unable to communicate with Tesla API: {ex.message}" + ) from ex _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message) return False diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py index 46bc49b126b..5a88999a7e3 100644 --- a/homeassistant/components/tesla/config_flow.py +++ b/homeassistant/components/tesla/config_flow.py @@ -175,7 +175,7 @@ async def validate_input(hass: core.HomeAssistant, data): if ex.code == HTTP_UNAUTHORIZED: _LOGGER.error("Invalid credentials: %s", ex) raise InvalidAuth() from ex - _LOGGER.error("Unable to communicate with Tesla API: %s", ex) + _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message) raise CannotConnect() from ex finally: await async_client.aclose() From aa590415d34891898c5c49b20a7b808ceff93b82 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 15 Aug 2021 21:33:48 +0200 Subject: [PATCH 204/355] Bump py-synologydsm-api to 1.0.4 (#54610) --- homeassistant/components/synology_dsm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 04d7f43bb75..8d8d30c2cf8 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -2,7 +2,7 @@ "domain": "synology_dsm", "name": "Synology DSM", "documentation": "https://www.home-assistant.io/integrations/synology_dsm", - "requirements": ["py-synologydsm-api==1.0.3"], + "requirements": ["py-synologydsm-api==1.0.4"], "codeowners": ["@hacf-fr", "@Quentame", "@mib1185"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 32f92439be8..ec0335cc819 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1257,7 +1257,7 @@ py-nightscout==1.2.2 py-schluter==0.1.7 # homeassistant.components.synology_dsm -py-synologydsm-api==1.0.3 +py-synologydsm-api==1.0.4 # homeassistant.components.zabbix py-zabbix==1.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 315ee031f03..e290fa4d4ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -705,7 +705,7 @@ py-melissa-climate==2.1.4 py-nightscout==1.2.2 # homeassistant.components.synology_dsm -py-synologydsm-api==1.0.3 +py-synologydsm-api==1.0.4 # homeassistant.components.seventeentrack py17track==3.2.1 From d0cebe911c1b13143ee041aec179902c50d82675 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 15 Aug 2021 16:06:05 -0400 Subject: [PATCH 205/355] Add siren, number, and weather to base platform list (#54665) --- homeassistant/setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 07bbaa22954..9575a4331b8 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -39,14 +39,17 @@ BASE_PLATFORMS = { "lock", "media_player", "notify", + "number", "remote", "scene", "select", "sensor", + "siren", "switch", "tts", "vacuum", "water_heater", + "weather", } DATA_SETUP_DONE = "setup_done" From 61412db119de805dc267a797c1f36b2c6ac17089 Mon Sep 17 00:00:00 2001 From: Nikolaos Stamatopoulos Date: Sun, 15 Aug 2021 22:56:30 +0200 Subject: [PATCH 206/355] Fix typo in xiaomi_miio cloud_login_error string (#54661) * fix(xiaomi_miio): Fix typo in cloud_login_error string * fixup! fix(xiaomi_miio): Fix typo in cloud_login_error string Restore translation files --- homeassistant/components/xiaomi_miio/gateway.py | 2 +- homeassistant/components/xiaomi_miio/strings.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/gateway.py b/homeassistant/components/xiaomi_miio/gateway.py index 17f42f4bffa..8b7a5c77a17 100644 --- a/homeassistant/components/xiaomi_miio/gateway.py +++ b/homeassistant/components/xiaomi_miio/gateway.py @@ -117,7 +117,7 @@ class ConnectXiaomiGateway: miio_cloud = MiCloud(self._cloud_username, self._cloud_password) if not miio_cloud.login(): raise ConfigEntryAuthFailed( - "Could not login to Xioami Miio Cloud, check the credentials" + "Could not login to Xiaomi Miio Cloud, check the credentials" ) devices_raw = miio_cloud.get_devices(self._cloud_country) self._gateway_device.get_devices_from_dict(devices_raw) diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json index 69a1621c973..129f6f1ecbf 100644 --- a/homeassistant/components/xiaomi_miio/strings.json +++ b/homeassistant/components/xiaomi_miio/strings.json @@ -12,7 +12,7 @@ "unknown_device": "The device model is not known, not able to setup the device using config flow.", "cloud_no_devices": "No devices found in this Xiaomi Miio cloud account.", "cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country", - "cloud_login_error": "Could not login to Xioami Miio Cloud, check the credentials." + "cloud_login_error": "Could not login to Xiaomi Miio Cloud, check the credentials." }, "flow_title": "{name}", "step": { From d01addbd249a7b1d494ecb66487967aab826e9d4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Aug 2021 18:27:10 -0500 Subject: [PATCH 207/355] Bump zeroconf to 0.35.1 (#54666) --- 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 b971ec06179..05576accb78 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.35.0"], + "requirements": ["zeroconf==0.35.1"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3f7eeb65ab5..9409432e13f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ sqlalchemy==1.4.17 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 -zeroconf==0.35.0 +zeroconf==0.35.1 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index ec0335cc819..3483f26245b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2439,7 +2439,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.35.0 +zeroconf==0.35.1 # homeassistant.components.zha zha-quirks==0.0.59 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e290fa4d4ee..bc0d8482421 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1347,7 +1347,7 @@ youless-api==0.10 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.35.0 +zeroconf==0.35.1 # homeassistant.components.zha zha-quirks==0.0.59 From 9e2945680efc4421a01942053920caf234ae7804 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 16 Aug 2021 01:32:01 +0200 Subject: [PATCH 208/355] Address late review of nut integration (#54606) * remove defaults from SensorEntityDescription * use _attr_unique_id instead of unique_id() * check if unique_id is not None --- homeassistant/components/nut/const.py | 147 ------------------------- homeassistant/components/nut/sensor.py | 9 +- 2 files changed, 2 insertions(+), 154 deletions(-) diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index 5bdd9049456..a180c2224f7 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -51,32 +51,22 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.status.display": SensorEntityDescription( key="ups.status.display", name="Status", - native_unit_of_measurement=None, icon="mdi:information-outline", - device_class=None, - state_class=None, ), "ups.status": SensorEntityDescription( key="ups.status", name="Status Data", - native_unit_of_measurement=None, icon="mdi:information-outline", - device_class=None, - state_class=None, ), "ups.alarm": SensorEntityDescription( key="ups.alarm", name="Alarms", - native_unit_of_measurement=None, icon="mdi:alarm", - device_class=None, - state_class=None, ), "ups.temperature": SensorEntityDescription( key="ups.temperature", name="UPS Temperature", native_unit_of_measurement=TEMP_CELSIUS, - icon=None, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), @@ -85,7 +75,6 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Load", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", - device_class=None, state_class=STATE_CLASS_MEASUREMENT, ), "ups.load.high": SensorEntityDescription( @@ -93,111 +82,79 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Overload Setting", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", - device_class=None, - state_class=None, ), "ups.id": SensorEntityDescription( key="ups.id", name="System identifier", - native_unit_of_measurement=None, icon="mdi:information-outline", - device_class=None, - state_class=None, ), "ups.delay.start": SensorEntityDescription( key="ups.delay.start", name="Load Restart Delay", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - device_class=None, - state_class=None, ), "ups.delay.reboot": SensorEntityDescription( key="ups.delay.reboot", name="UPS Reboot Delay", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - device_class=None, - state_class=None, ), "ups.delay.shutdown": SensorEntityDescription( key="ups.delay.shutdown", name="UPS Shutdown Delay", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - device_class=None, - state_class=None, ), "ups.timer.start": SensorEntityDescription( key="ups.timer.start", name="Load Start Timer", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - device_class=None, - state_class=None, ), "ups.timer.reboot": SensorEntityDescription( key="ups.timer.reboot", name="Load Reboot Timer", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - device_class=None, - state_class=None, ), "ups.timer.shutdown": SensorEntityDescription( key="ups.timer.shutdown", name="Load Shutdown Timer", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - device_class=None, - state_class=None, ), "ups.test.interval": SensorEntityDescription( key="ups.test.interval", name="Self-Test Interval", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - device_class=None, - state_class=None, ), "ups.test.result": SensorEntityDescription( key="ups.test.result", name="Self-Test Result", - native_unit_of_measurement=None, icon="mdi:information-outline", - device_class=None, - state_class=None, ), "ups.test.date": SensorEntityDescription( key="ups.test.date", name="Self-Test Date", - native_unit_of_measurement=None, icon="mdi:calendar", - device_class=None, - state_class=None, ), "ups.display.language": SensorEntityDescription( key="ups.display.language", name="Language", - native_unit_of_measurement=None, icon="mdi:information-outline", - device_class=None, - state_class=None, ), "ups.contacts": SensorEntityDescription( key="ups.contacts", name="External Contacts", - native_unit_of_measurement=None, icon="mdi:information-outline", - device_class=None, - state_class=None, ), "ups.efficiency": SensorEntityDescription( key="ups.efficiency", name="Efficiency", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", - device_class=None, state_class=STATE_CLASS_MEASUREMENT, ), "ups.power": SensorEntityDescription( @@ -205,7 +162,6 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Current Apparent Power", native_unit_of_measurement=POWER_VOLT_AMPERE, icon="mdi:flash", - device_class=None, state_class=STATE_CLASS_MEASUREMENT, ), "ups.power.nominal": SensorEntityDescription( @@ -213,14 +169,11 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Nominal Power", native_unit_of_measurement=POWER_VOLT_AMPERE, icon="mdi:flash", - device_class=None, - state_class=None, ), "ups.realpower": SensorEntityDescription( key="ups.realpower", name="Current Real Power", native_unit_of_measurement=POWER_WATT, - icon=None, device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), @@ -228,71 +181,47 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="ups.realpower.nominal", name="Nominal Real Power", native_unit_of_measurement=POWER_WATT, - icon=None, device_class=DEVICE_CLASS_POWER, - state_class=None, ), "ups.beeper.status": SensorEntityDescription( key="ups.beeper.status", name="Beeper Status", - native_unit_of_measurement=None, icon="mdi:information-outline", - device_class=None, - state_class=None, ), "ups.type": SensorEntityDescription( key="ups.type", name="UPS Type", - native_unit_of_measurement=None, icon="mdi:information-outline", - device_class=None, - state_class=None, ), "ups.watchdog.status": SensorEntityDescription( key="ups.watchdog.status", name="Watchdog Status", - native_unit_of_measurement=None, icon="mdi:information-outline", - device_class=None, - state_class=None, ), "ups.start.auto": SensorEntityDescription( key="ups.start.auto", name="Start on AC", - native_unit_of_measurement=None, icon="mdi:information-outline", - device_class=None, - state_class=None, ), "ups.start.battery": SensorEntityDescription( key="ups.start.battery", name="Start on Battery", - native_unit_of_measurement=None, icon="mdi:information-outline", - device_class=None, - state_class=None, ), "ups.start.reboot": SensorEntityDescription( key="ups.start.reboot", name="Reboot on Battery", - native_unit_of_measurement=None, icon="mdi:information-outline", - device_class=None, - state_class=None, ), "ups.shutdown": SensorEntityDescription( key="ups.shutdown", name="Shutdown Ability", - native_unit_of_measurement=None, icon="mdi:information-outline", - device_class=None, - state_class=None, ), "battery.charge": SensorEntityDescription( key="battery.charge", name="Battery Charge", native_unit_of_measurement=PERCENTAGE, - icon=None, device_class=DEVICE_CLASS_BATTERY, state_class=STATE_CLASS_MEASUREMENT, ), @@ -301,38 +230,28 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Low Battery Setpoint", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", - device_class=None, - state_class=None, ), "battery.charge.restart": SensorEntityDescription( key="battery.charge.restart", name="Minimum Battery to Start", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", - device_class=None, - state_class=None, ), "battery.charge.warning": SensorEntityDescription( key="battery.charge.warning", name="Warning Battery Setpoint", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", - device_class=None, - state_class=None, ), "battery.charger.status": SensorEntityDescription( key="battery.charger.status", name="Charging Status", - native_unit_of_measurement=None, icon="mdi:information-outline", - device_class=None, - state_class=None, ), "battery.voltage": SensorEntityDescription( key="battery.voltage", name="Battery Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - icon=None, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, ), @@ -340,40 +259,31 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="battery.voltage.nominal", name="Nominal Battery Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - icon=None, device_class=DEVICE_CLASS_VOLTAGE, - state_class=None, ), "battery.voltage.low": SensorEntityDescription( key="battery.voltage.low", name="Low Battery Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - icon=None, device_class=DEVICE_CLASS_VOLTAGE, - state_class=None, ), "battery.voltage.high": SensorEntityDescription( key="battery.voltage.high", name="High Battery Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - icon=None, device_class=DEVICE_CLASS_VOLTAGE, - state_class=None, ), "battery.capacity": SensorEntityDescription( key="battery.capacity", name="Battery Capacity", native_unit_of_measurement="Ah", icon="mdi:flash", - device_class=None, - state_class=None, ), "battery.current": SensorEntityDescription( key="battery.current", name="Battery Current", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, icon="mdi:flash", - device_class=None, state_class=STATE_CLASS_MEASUREMENT, ), "battery.current.total": SensorEntityDescription( @@ -381,14 +291,11 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Total Battery Current", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, icon="mdi:flash", - device_class=None, - state_class=None, ), "battery.temperature": SensorEntityDescription( key="battery.temperature", name="Battery Temperature", native_unit_of_measurement=TEMP_CELSIUS, - icon=None, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), @@ -397,110 +304,75 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Battery Runtime", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - device_class=None, - state_class=None, ), "battery.runtime.low": SensorEntityDescription( key="battery.runtime.low", name="Low Battery Runtime", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - device_class=None, - state_class=None, ), "battery.runtime.restart": SensorEntityDescription( key="battery.runtime.restart", name="Minimum Battery Runtime to Start", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", - device_class=None, - state_class=None, ), "battery.alarm.threshold": SensorEntityDescription( key="battery.alarm.threshold", name="Battery Alarm Threshold", - native_unit_of_measurement=None, icon="mdi:information-outline", - device_class=None, - state_class=None, ), "battery.date": SensorEntityDescription( key="battery.date", name="Battery Date", - native_unit_of_measurement=None, icon="mdi:calendar", - device_class=None, - state_class=None, ), "battery.mfr.date": SensorEntityDescription( key="battery.mfr.date", name="Battery Manuf. Date", - native_unit_of_measurement=None, icon="mdi:calendar", - device_class=None, - state_class=None, ), "battery.packs": SensorEntityDescription( key="battery.packs", name="Number of Batteries", - native_unit_of_measurement=None, icon="mdi:information-outline", - device_class=None, - state_class=None, ), "battery.packs.bad": SensorEntityDescription( key="battery.packs.bad", name="Number of Bad Batteries", - native_unit_of_measurement=None, icon="mdi:information-outline", - device_class=None, - state_class=None, ), "battery.type": SensorEntityDescription( key="battery.type", name="Battery Chemistry", - native_unit_of_measurement=None, icon="mdi:information-outline", - device_class=None, - state_class=None, ), "input.sensitivity": SensorEntityDescription( key="input.sensitivity", name="Input Power Sensitivity", - native_unit_of_measurement=None, icon="mdi:information-outline", - device_class=None, - state_class=None, ), "input.transfer.low": SensorEntityDescription( key="input.transfer.low", name="Low Voltage Transfer", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - icon=None, device_class=DEVICE_CLASS_VOLTAGE, - state_class=None, ), "input.transfer.high": SensorEntityDescription( key="input.transfer.high", name="High Voltage Transfer", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - icon=None, device_class=DEVICE_CLASS_VOLTAGE, - state_class=None, ), "input.transfer.reason": SensorEntityDescription( key="input.transfer.reason", name="Voltage Transfer Reason", - native_unit_of_measurement=None, icon="mdi:information-outline", - device_class=None, - state_class=None, ), "input.voltage": SensorEntityDescription( key="input.voltage", name="Input Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - icon=None, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, ), @@ -508,16 +380,13 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="input.voltage.nominal", name="Nominal Input Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - icon=None, device_class=DEVICE_CLASS_VOLTAGE, - state_class=None, ), "input.frequency": SensorEntityDescription( key="input.frequency", name="Input Line Frequency", native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:flash", - device_class=None, state_class=STATE_CLASS_MEASUREMENT, ), "input.frequency.nominal": SensorEntityDescription( @@ -525,23 +394,17 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Nominal Input Line Frequency", native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:flash", - device_class=None, - state_class=None, ), "input.frequency.status": SensorEntityDescription( key="input.frequency.status", name="Input Frequency Status", - native_unit_of_measurement=None, icon="mdi:information-outline", - device_class=None, - state_class=None, ), "output.current": SensorEntityDescription( key="output.current", name="Output Current", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, icon="mdi:flash", - device_class=None, state_class=STATE_CLASS_MEASUREMENT, ), "output.current.nominal": SensorEntityDescription( @@ -549,14 +412,11 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Nominal Output Current", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, icon="mdi:flash", - device_class=None, - state_class=None, ), "output.voltage": SensorEntityDescription( key="output.voltage", name="Output Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - icon=None, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, ), @@ -564,16 +424,13 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="output.voltage.nominal", name="Nominal Output Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - icon=None, device_class=DEVICE_CLASS_VOLTAGE, - state_class=None, ), "output.frequency": SensorEntityDescription( key="output.frequency", name="Output Frequency", native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:flash", - device_class=None, state_class=STATE_CLASS_MEASUREMENT, ), "output.frequency.nominal": SensorEntityDescription( @@ -581,14 +438,11 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Nominal Output Frequency", native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:flash", - device_class=None, - state_class=None, ), "ambient.humidity": SensorEntityDescription( key="ambient.humidity", name="Ambient Humidity", native_unit_of_measurement=PERCENTAGE, - icon=None, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), @@ -596,7 +450,6 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="ambient.temperature", name="Ambient Temperature", native_unit_of_measurement=TEMP_CELSIUS, - icon=None, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 971c194c1c9..995032eb0fd 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -104,6 +104,8 @@ class NUTSensor(CoordinatorEntity, SensorEntity): self._unique_id = unique_id self._attr_name = f"{name} {sensor_description.name}" + if unique_id is not None: + self._attr_unique_id = f"{unique_id}_{sensor_description.key}" @property def device_info(self): @@ -122,13 +124,6 @@ class NUTSensor(CoordinatorEntity, SensorEntity): device_info["sw_version"] = self._firmware return device_info - @property - def unique_id(self): - """Sensor Unique id.""" - if not self._unique_id: - return None - return f"{self._unique_id}_{self.entity_description.key}" - @property def native_value(self): """Return entity state from ups.""" From d091068092c80332c8c15e61e3f7699d0e1efd3a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 16 Aug 2021 00:11:52 +0000 Subject: [PATCH 209/355] [ci skip] Translation update --- homeassistant/components/xiaomi_miio/translations/en.json | 2 +- .../components/xiaomi_miio/translations/select.ru.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/translations/en.json b/homeassistant/components/xiaomi_miio/translations/en.json index cbe10230093..84593a3edc1 100644 --- a/homeassistant/components/xiaomi_miio/translations/en.json +++ b/homeassistant/components/xiaomi_miio/translations/en.json @@ -10,7 +10,7 @@ "error": { "cannot_connect": "Failed to connect", "cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country", - "cloud_login_error": "Could not login to Xioami Miio Cloud, check the credentials.", + "cloud_login_error": "Could not login to Xiaomi Miio Cloud, check the credentials.", "cloud_no_devices": "No devices found in this Xiaomi Miio cloud account.", "no_device_selected": "No device selected, please select one device.", "unknown_device": "The device model is not known, not able to setup the device using config flow." diff --git a/homeassistant/components/xiaomi_miio/translations/select.ru.json b/homeassistant/components/xiaomi_miio/translations/select.ru.json index 4dac3002d1b..138d2b4fdce 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.ru.json +++ b/homeassistant/components/xiaomi_miio/translations/select.ru.json @@ -3,7 +3,7 @@ "xiaomi_miio__led_brightness": { "bright": "\u042f\u0440\u043a\u043e", "dim": "\u0422\u0443\u0441\u043a\u043b\u043e", - "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "off": "\u041e\u0442\u043a\u043b." } } } \ No newline at end of file From 41d932fcf8d1ba34a2064bacb1f1f78822d9286d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 16 Aug 2021 14:56:21 +1200 Subject: [PATCH 210/355] Send color_brightness to ESPHome devices on 1.20 (pre-color_mode) (#54670) --- homeassistant/components/esphome/light.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index aeecc22d9f1..c6cf9742082 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -105,8 +105,8 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): color_bri = max(rgb) # normalize rgb data["rgb"] = tuple(x / (color_bri or 1) for x in rgb) + data["color_brightness"] = color_bri if self._supports_color_mode: - data["color_brightness"] = color_bri data["color_mode"] = LightColorMode.RGB if (rgbw_ha := kwargs.get(ATTR_RGBW_COLOR)) is not None: @@ -116,8 +116,8 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): # normalize rgb data["rgb"] = tuple(x / (color_bri or 1) for x in rgb) data["white"] = w + data["color_brightness"] = color_bri if self._supports_color_mode: - data["color_brightness"] = color_bri data["color_mode"] = LightColorMode.RGB_WHITE if (rgbww_ha := kwargs.get(ATTR_RGBWW_COLOR)) is not None: @@ -144,8 +144,8 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): data["color_temperature"] = min_ct + ct_ratio * (max_ct - min_ct) target_mode = LightColorMode.RGB_COLOR_TEMPERATURE + data["color_brightness"] = color_bri if self._supports_color_mode: - data["color_brightness"] = color_bri data["color_mode"] = target_mode if (flash := kwargs.get(ATTR_FLASH)) is not None: From bab7d46c9bf4b46afe9af138f11d933cbedbc3f8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 15 Aug 2021 19:56:56 -0700 Subject: [PATCH 211/355] Guard partial upgrade (#54617) --- homeassistant/components/http/forwarded.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/http/forwarded.py b/homeassistant/components/http/forwarded.py index 9a76866ba21..6dd2d9adb8a 100644 --- a/homeassistant/components/http/forwarded.py +++ b/homeassistant/components/http/forwarded.py @@ -65,6 +65,11 @@ def async_setup_forwarded( try: from hass_nabucasa import remote # pylint: disable=import-outside-toplevel + + # venv users might have already loaded it before it got upgraded so guard for this + # This can only happen when people upgrade from before 2021.8.5. + if not hasattr(remote, "is_cloud_request"): + remote = None except ImportError: remote = None From b2f73b3c69f834f3b10537fce42b87bfb054dcf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Mon, 16 Aug 2021 04:57:18 +0200 Subject: [PATCH 212/355] Fix Tibber last reset (#54582) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tibber/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 6674f6829f9..15bf2a2017e 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -137,6 +137,7 @@ RT_SENSOR_MAP: dict[str, TibberSensorEntityDescription] = { device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, + reset_type=ResetType.NEVER, ), "lastMeterProduction": TibberSensorEntityDescription( key="lastMeterProduction", @@ -144,6 +145,7 @@ RT_SENSOR_MAP: dict[str, TibberSensorEntityDescription] = { device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT, + reset_type=ResetType.NEVER, ), "voltagePhase1": TibberSensorEntityDescription( key="voltagePhase1", From bec42b74feddf5a38171ca1333d8c64de6bd6f90 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 16 Aug 2021 04:57:37 +0200 Subject: [PATCH 213/355] Solve switch/verify register type convert problem in modbus (#54645) --- homeassistant/components/modbus/base_platform.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index c580e6167ca..468e61aefa8 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -187,9 +187,9 @@ class BaseSwitch(BasePlatform, ToggleEntity, RestoreEntity): self._verify_address = config[CONF_VERIFY].get( CONF_ADDRESS, config[CONF_ADDRESS] ) - self._verify_type = config[CONF_VERIFY].get( - CONF_INPUT_TYPE, convert[config[CONF_WRITE_TYPE]][0] - ) + self._verify_type = convert[ + config[CONF_VERIFY].get(CONF_INPUT_TYPE, config[CONF_WRITE_TYPE]) + ][0] self._state_on = config[CONF_VERIFY].get(CONF_STATE_ON, self.command_on) self._state_off = config[CONF_VERIFY].get(CONF_STATE_OFF, self._command_off) else: From 094f7d38ad5b30f0dbedeea4492b7a85f3c0c228 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 15 Aug 2021 21:02:37 -0700 Subject: [PATCH 214/355] Use buffer at stream start with unsupported audio (#54672) Add a test that reproduces the issue where resetting the iterator drops the already read packets. Fix a bug in replace_underlying_iterator because checking the self._next function turns out not to work since it points to a bound method so the "is not" check fails. --- homeassistant/components/stream/worker.py | 2 +- tests/components/stream/test_worker.py | 52 ++++++++++++++--------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 69def43b2a2..039163c6cf5 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -225,7 +225,7 @@ class PeekIterator(Iterator): def replace_underlying_iterator(self, new_iterator: Iterator) -> None: """Replace the underlying iterator while preserving the buffer.""" self._iterator = new_iterator - if self._next is not self._pop_buffer: + if not self._buffer: self._next = self._iterator.__next__ def _pop_buffer(self) -> av.Packet: diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index ffbeb44d79e..e62a190d7be 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -15,6 +15,7 @@ failure modes or corner cases like how out of order packets are handled. import fractions import io +import logging import math import threading from unittest.mock import patch @@ -52,7 +53,7 @@ SEGMENTS_PER_PACKET = PACKET_DURATION / SEGMENT_DURATION TIMEOUT = 15 -class FakePyAvStream: +class FakeAvInputStream: """A fake pyav Stream.""" def __init__(self, name, rate): @@ -66,9 +67,13 @@ class FakePyAvStream: self.codec = FakeCodec() + def __str__(self) -> str: + """Return a stream name for debugging.""" + return f"FakePyAvStream<{self.name}, {self.time_base}>" -VIDEO_STREAM = FakePyAvStream(VIDEO_STREAM_FORMAT, VIDEO_FRAME_RATE) -AUDIO_STREAM = FakePyAvStream(AUDIO_STREAM_FORMAT, AUDIO_SAMPLE_RATE) + +VIDEO_STREAM = FakeAvInputStream(VIDEO_STREAM_FORMAT, VIDEO_FRAME_RATE) +AUDIO_STREAM = FakeAvInputStream(AUDIO_STREAM_FORMAT, AUDIO_SAMPLE_RATE) class PacketSequence: @@ -110,6 +115,9 @@ class PacketSequence: is_keyframe = not (self.packet - 1) % (VIDEO_FRAME_RATE * KEYFRAME_INTERVAL) size = 3 + def __str__(self) -> str: + return f"FakePacket" + return FakePacket() @@ -154,7 +162,7 @@ class FakePyAvBuffer: def add_stream(self, template=None): """Create an output buffer that captures packets for test to examine.""" - class FakeStream: + class FakeAvOutputStream: def __init__(self, capture_packets): self.capture_packets = capture_packets @@ -162,11 +170,15 @@ class FakePyAvBuffer: return def mux(self, packet): + logging.debug("Muxed packet: %s", packet) self.capture_packets.append(packet) + def __str__(self) -> str: + return f"FakeAvOutputStream<{template.name}>" + if template.name == AUDIO_STREAM_FORMAT: - return FakeStream(self.audio_packets) - return FakeStream(self.video_packets) + return FakeAvOutputStream(self.audio_packets) + return FakeAvOutputStream(self.video_packets) def mux(self, packet): """Capture a packet for tests to examine.""" @@ -217,7 +229,7 @@ async def async_decode_stream(hass, packets, py_av=None): if not py_av: py_av = MockPyAv() - py_av.container.packets = packets + py_av.container.packets = iter(packets) # Can't be rewound with patch("av.open", new=py_av.open), patch( "homeassistant.components.stream.core.StreamOutput.put", @@ -273,7 +285,7 @@ async def test_skip_out_of_order_packet(hass): assert not packets[out_of_order_index].is_keyframe packets[out_of_order_index].dts = -9090 - decoded_stream = await async_decode_stream(hass, iter(packets)) + decoded_stream = await async_decode_stream(hass, packets) segments = decoded_stream.segments complete_segments = decoded_stream.complete_segments # Check sequence numbers @@ -309,7 +321,7 @@ async def test_discard_old_packets(hass): # Packets after this one are considered out of order packets[OUT_OF_ORDER_PACKET_INDEX - 1].dts = 9090 - decoded_stream = await async_decode_stream(hass, iter(packets)) + decoded_stream = await async_decode_stream(hass, packets) segments = decoded_stream.segments complete_segments = decoded_stream.complete_segments # Check number of segments @@ -331,7 +343,7 @@ async def test_packet_overflow(hass): # Packet is so far out of order, exceeds max gap and looks like overflow packets[OUT_OF_ORDER_PACKET_INDEX].dts = -9000000 - decoded_stream = await async_decode_stream(hass, iter(packets)) + decoded_stream = await async_decode_stream(hass, packets) segments = decoded_stream.segments complete_segments = decoded_stream.complete_segments # Check number of segments @@ -355,7 +367,7 @@ async def test_skip_initial_bad_packets(hass): for i in range(0, num_bad_packets): packets[i].dts = None - decoded_stream = await async_decode_stream(hass, iter(packets)) + decoded_stream = await async_decode_stream(hass, packets) segments = decoded_stream.segments complete_segments = decoded_stream.complete_segments # Check sequence numbers @@ -385,7 +397,7 @@ async def test_too_many_initial_bad_packets_fails(hass): for i in range(0, num_bad_packets): packets[i].dts = None - decoded_stream = await async_decode_stream(hass, iter(packets)) + decoded_stream = await async_decode_stream(hass, packets) segments = decoded_stream.segments assert len(segments) == 0 assert len(decoded_stream.video_packets) == 0 @@ -405,7 +417,7 @@ async def test_skip_missing_dts(hass): continue packets[i].dts = None - decoded_stream = await async_decode_stream(hass, iter(packets)) + decoded_stream = await async_decode_stream(hass, packets) segments = decoded_stream.segments complete_segments = decoded_stream.complete_segments # Check sequence numbers @@ -426,7 +438,7 @@ async def test_too_many_bad_packets(hass): for i in range(bad_packet_start, bad_packet_start + num_bad_packets): packets[i].dts = None - decoded_stream = await async_decode_stream(hass, iter(packets)) + decoded_stream = await async_decode_stream(hass, packets) complete_segments = decoded_stream.complete_segments assert len(complete_segments) == int((bad_packet_start - 1) * SEGMENTS_PER_PACKET) assert len(decoded_stream.video_packets) == bad_packet_start @@ -454,7 +466,7 @@ async def test_audio_packets_not_found(hass): num_packets = PACKETS_TO_WAIT_FOR_AUDIO + 1 packets = PacketSequence(num_packets) # Contains only video packets - decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av) + decoded_stream = await async_decode_stream(hass, packets, py_av=py_av) complete_segments = decoded_stream.complete_segments assert len(complete_segments) == int((num_packets - 1) * SEGMENTS_PER_PACKET) assert len(decoded_stream.video_packets) == num_packets @@ -474,8 +486,10 @@ async def test_adts_aac_audio(hass): packets[1][0] = 255 packets[1][1] = 241 - decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av) + decoded_stream = await async_decode_stream(hass, packets, py_av=py_av) assert len(decoded_stream.audio_packets) == 0 + # All decoded video packets are still preserved + assert len(decoded_stream.video_packets) == num_packets - 1 async def test_audio_is_first_packet(hass): @@ -493,7 +507,7 @@ async def test_audio_is_first_packet(hass): packets[2].dts = int(packets[3].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) packets[2].pts = int(packets[3].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) - decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av) + decoded_stream = await async_decode_stream(hass, packets, py_av=py_av) complete_segments = decoded_stream.complete_segments # The audio packets are segmented with the video packets assert len(complete_segments) == int((num_packets - 2 - 1) * SEGMENTS_PER_PACKET) @@ -511,7 +525,7 @@ async def test_audio_packets_found(hass): packets[1].dts = int(packets[0].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) packets[1].pts = int(packets[0].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) - decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av) + decoded_stream = await async_decode_stream(hass, packets, py_av=py_av) complete_segments = decoded_stream.complete_segments # The audio packet above is buffered with the video packet assert len(complete_segments) == int((num_packets - 1 - 1) * SEGMENTS_PER_PACKET) @@ -529,7 +543,7 @@ async def test_pts_out_of_order(hass): packets[i].pts = packets[i - 1].pts - 1 packets[i].is_keyframe = False - decoded_stream = await async_decode_stream(hass, iter(packets)) + decoded_stream = await async_decode_stream(hass, packets) segments = decoded_stream.segments complete_segments = decoded_stream.complete_segments # Check number of segments From 416668c2890fb60691b9e19b373f2ec2c6eacbd6 Mon Sep 17 00:00:00 2001 From: cnico Date: Mon, 16 Aug 2021 07:12:39 +0200 Subject: [PATCH 215/355] Address late review of Flipr (#54668) * Code format correction * Other code review remarks of MartinHjelmare * Simplification of flipr instantiation * Formatting correction --- homeassistant/components/flipr/__init__.py | 4 +--- homeassistant/components/flipr/config_flow.py | 7 +++---- homeassistant/components/flipr/sensor.py | 5 +++-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/flipr/__init__.py b/homeassistant/components/flipr/__init__.py index 05bbd0d5449..66ea93484f7 100644 --- a/homeassistant/components/flipr/__init__.py +++ b/homeassistant/components/flipr/__init__.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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) @@ -54,8 +54,6 @@ class FliprDataUpdateCoordinator(DataUpdateCoordinator): password = entry.data[CONF_PASSWORD] self.flipr_id = entry.data[CONF_FLIPR_ID] - _LOGGER.debug("Config entry values : %s, %s", username, self.flipr_id) - # Establishes the connection. self.client = FliprAPIRestClient(username, password) self.entry = entry diff --git a/homeassistant/components/flipr/config_flow.py b/homeassistant/components/flipr/config_flow.py index b503281fed4..b1e4f31d044 100644 --- a/homeassistant/components/flipr/config_flow.py +++ b/homeassistant/components/flipr/config_flow.py @@ -45,7 +45,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" _LOGGER.exception(exception) - if not errors and len(flipr_ids) == 0: + if not errors and not flipr_ids: # No flipr_id found. Tell the user with an error message. errors["base"] = "no_flipr_id_found" @@ -85,9 +85,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _authenticate_and_search_flipr(self) -> list[str]: """Validate the username and password provided and searches for a flipr id.""" - client = await self.hass.async_add_executor_job( - FliprAPIRestClient, self._username, self._password - ) + # Instantiates the flipr API that does not require async since it is has no network access. + client = FliprAPIRestClient(self._username, self._password) flipr_ids = await self.hass.async_add_executor_job(client.search_flipr_ids) diff --git a/homeassistant/components/flipr/sensor.py b/homeassistant/components/flipr/sensor.py index 3a02b7e2f2e..f9fd4e9633e 100644 --- a/homeassistant/components/flipr/sensor.py +++ b/homeassistant/components/flipr/sensor.py @@ -6,6 +6,7 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + ELECTRIC_POTENTIAL_MILLIVOLT, TEMP_CELSIUS, ) @@ -14,7 +15,7 @@ from .const import ATTRIBUTION, CONF_FLIPR_ID, DOMAIN SENSORS = { "chlorine": { - "unit": "mV", + "unit": ELECTRIC_POTENTIAL_MILLIVOLT, "icon": "mdi:pool", "name": "Chlorine", "device_class": None, @@ -33,7 +34,7 @@ SENSORS = { "device_class": DEVICE_CLASS_TIMESTAMP, }, "red_ox": { - "unit": "mV", + "unit": ELECTRIC_POTENTIAL_MILLIVOLT, "icon": "mdi:pool", "name": "Red OX", "device_class": None, From 8c4a2dc6d2f3dc047eacba34eb2de66a4e5d43a9 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 16 Aug 2021 09:13:45 +0200 Subject: [PATCH 216/355] Add `water level` and `water tank detached` sensors for Xiaomi Miio humidifiers (#54625) * Add water level and water tank detached sensors * Use elif * Use DEVICE_CLASS_CONNECTIVITY for water tank sensor * Improve docstring * Change the water tank sensor icon * Fix typo --- .../components/xiaomi_miio/binary_sensor.py | 39 ++++++++++++++++--- .../components/xiaomi_miio/sensor.py | 9 ++++- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index c2f14b17d22..6254c00916e 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -1,7 +1,12 @@ """Support for Xiaomi Miio binary sensors.""" +from __future__ import annotations + +from dataclasses import dataclass from enum import Enum +from typing import Callable from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -13,6 +18,8 @@ from .const import ( DOMAIN, KEY_COORDINATOR, KEY_DEVICE, + MODELS_HUMIDIFIER_MIIO, + MODELS_HUMIDIFIER_MIOT, MODELS_HUMIDIFIER_MJJSQ, ) from .device import XiaomiCoordinatedMiioEntity @@ -20,19 +27,31 @@ from .device import XiaomiCoordinatedMiioEntity ATTR_NO_WATER = "no_water" ATTR_WATER_TANK_DETACHED = "water_tank_detached" + +@dataclass +class XiaomiMiioBinarySensorDescription(BinarySensorEntityDescription): + """A class that describes binary sensor entities.""" + + value: Callable | None = None + + BINARY_SENSOR_TYPES = ( - BinarySensorEntityDescription( + XiaomiMiioBinarySensorDescription( key=ATTR_NO_WATER, name="Water Tank Empty", icon="mdi:water-off-outline", ), - BinarySensorEntityDescription( + XiaomiMiioBinarySensorDescription( key=ATTR_WATER_TANK_DETACHED, - name="Water Tank Detached", - icon="mdi:flask-empty-off-outline", + name="Water Tank", + icon="mdi:car-coolant-level", + device_class=DEVICE_CLASS_CONNECTIVITY, + value=lambda value: not value, ), ) +HUMIDIFIER_MIIO_BINARY_SENSORS = (ATTR_WATER_TANK_DETACHED,) +HUMIDIFIER_MIOT_BINARY_SENSORS = (ATTR_WATER_TANK_DETACHED,) HUMIDIFIER_MJJSQ_BINARY_SENSORS = (ATTR_NO_WATER, ATTR_WATER_TANK_DETACHED) @@ -43,7 +62,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: model = config_entry.data[CONF_MODEL] sensors = [] - if model in MODELS_HUMIDIFIER_MJJSQ: + if model in MODELS_HUMIDIFIER_MIIO: + sensors = HUMIDIFIER_MIIO_BINARY_SENSORS + elif model in MODELS_HUMIDIFIER_MIOT: + sensors = HUMIDIFIER_MIOT_BINARY_SENSORS + elif model in MODELS_HUMIDIFIER_MJJSQ: sensors = HUMIDIFIER_MJJSQ_BINARY_SENSORS for description in BINARY_SENSOR_TYPES: if description.key not in sensors: @@ -74,9 +97,13 @@ class XiaomiGenericBinarySensor(XiaomiCoordinatedMiioEntity, BinarySensorEntity) @property def is_on(self): """Return true if the binary sensor is on.""" - return self._extract_value_from_attribute( + state = self._extract_value_from_attribute( self.coordinator.data, self.entity_description.key ) + if self.entity_description.value is not None and state is not None: + return self.entity_description.value(state) + + return state @staticmethod def _extract_value_from_attribute(state, attribute): diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 26914e1dff8..a8a2787aaed 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -234,8 +234,13 @@ SENSOR_TYPES = { ), } -HUMIDIFIER_MIIO_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE) -HUMIDIFIER_CA1_CB1_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE, ATTR_MOTOR_SPEED) +HUMIDIFIER_MIIO_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE, ATTR_WATER_LEVEL) +HUMIDIFIER_CA1_CB1_SENSORS = ( + ATTR_HUMIDITY, + ATTR_TEMPERATURE, + ATTR_MOTOR_SPEED, + ATTR_WATER_LEVEL, +) HUMIDIFIER_MIOT_SENSORS = ( ATTR_ACTUAL_SPEED, ATTR_HUMIDITY, From f684e4df349f7b95da473a1003f708ac67284848 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 16 Aug 2021 10:36:31 +0100 Subject: [PATCH 217/355] Add code owner to GitHub integration (#54689) --- CODEOWNERS | 1 + homeassistant/components/github/manifest.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index a7aea24c4f0..4b7cb8520b0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -187,6 +187,7 @@ homeassistant/components/geo_rss_events/* @exxamalte homeassistant/components/geonetnz_quakes/* @exxamalte homeassistant/components/geonetnz_volcano/* @exxamalte homeassistant/components/gios/* @bieniu +homeassistant/components/github/* @timmo001 homeassistant/components/gitter/* @fabaff homeassistant/components/glances/* @fabaff @engrbm87 homeassistant/components/goalzero/* @tkdrob diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index d4405196b7a..40693244b91 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -3,6 +3,6 @@ "name": "GitHub", "documentation": "https://www.home-assistant.io/integrations/github", "requirements": ["PyGithub==1.43.8"], - "codeowners": [], + "codeowners": ["@timmo001"], "iot_class": "cloud_polling" } From 045b1ca6ae5366060727131ce3fa791a5b966d85 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 16 Aug 2021 12:41:35 +0200 Subject: [PATCH 218/355] Activate mypy in lifx (#54540) --- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 2 files changed, 4 deletions(-) diff --git a/mypy.ini b/mypy.ini index 0cd2a419fe0..91b40b63cc1 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1466,9 +1466,6 @@ ignore_errors = true [mypy-homeassistant.components.kulersky.*] ignore_errors = true -[mypy-homeassistant.components.lifx.*] -ignore_errors = true - [mypy-homeassistant.components.litejet.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 188f2a0a41b..1b24a935084 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -81,7 +81,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.konnected.*", "homeassistant.components.kostal_plenticore.*", "homeassistant.components.kulersky.*", - "homeassistant.components.lifx.*", "homeassistant.components.litejet.*", "homeassistant.components.litterrobot.*", "homeassistant.components.lovelace.*", From 75275254f99a4f52492f09a608e26edf3277384d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 16 Aug 2021 12:52:58 +0200 Subject: [PATCH 219/355] Renault test optimisation (#53705) * Cleanup tests * Use a MockConfigEntry * Don't set up the integration for duplicate config entry testing --- tests/components/renault/__init__.py | 176 ++++++------ tests/components/renault/test_config_flow.py | 268 ++++++++++++------- tests/components/renault/test_init.py | 84 +++--- tests/components/renault/test_sensor.py | 79 ++---- 4 files changed, 314 insertions(+), 293 deletions(-) diff --git a/tests/components/renault/__init__.py b/tests/components/renault/__init__.py index e4edc3b8539..fcd190fe98d 100644 --- a/tests/components/renault/__init__.py +++ b/tests/components/renault/__init__.py @@ -1,52 +1,32 @@ """Tests for the Renault integration.""" from __future__ import annotations -from datetime import timedelta from typing import Any from unittest.mock import patch -from renault_api.kamereon import models, schemas -from renault_api.renault_vehicle import RenaultVehicle +from renault_api.kamereon import schemas +from renault_api.renault_account import RenaultAccount -from homeassistant.components.renault.const import ( - CONF_KAMEREON_ACCOUNT_ID, - CONF_LOCALE, - DOMAIN, -) -from homeassistant.components.renault.renault_vehicle import RenaultVehicleProxy -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.components.renault.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client -from .const import MOCK_VEHICLES +from .const import MOCK_CONFIG, MOCK_VEHICLES from tests.common import MockConfigEntry, load_fixture -async def setup_renault_integration(hass: HomeAssistant): +def get_mock_config_entry(): """Create the Renault integration.""" - config_entry = MockConfigEntry( + return MockConfigEntry( domain=DOMAIN, - source="user", - data={ - CONF_LOCALE: "fr_FR", - CONF_USERNAME: "email@test.com", - CONF_PASSWORD: "test", - CONF_KAMEREON_ACCOUNT_ID: "account_id_2", - }, - unique_id="account_id_2", + source=SOURCE_USER, + data=MOCK_CONFIG, + unique_id="account_id_1", options={}, - entry_id="1", + entry_id="123456", ) - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.components.renault.RenaultHub.attempt_login", return_value=True - ), patch("homeassistant.components.renault.RenaultHub.async_initialise"): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - return config_entry def get_fixtures(vehicle_type: str) -> dict[str, Any]: @@ -76,84 +56,116 @@ def get_fixtures(vehicle_type: str) -> dict[str, Any]: } -async def create_vehicle_proxy( - hass: HomeAssistant, vehicle_type: str -) -> RenaultVehicleProxy: - """Create a vehicle proxy for testing.""" +async def setup_renault_integration_simple(hass: HomeAssistant): + """Create the Renault integration.""" + config_entry = get_mock_config_entry() + config_entry.add_to_hass(hass) + + renault_account = RenaultAccount( + config_entry.unique_id, + websession=aiohttp_client.async_get_clientsession(hass), + ) + + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_client.RenaultClient.get_api_account", + return_value=renault_account, + ), patch("renault_api.renault_account.RenaultAccount.get_vehicles"): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry + + +async def setup_renault_integration_vehicle(hass: HomeAssistant, vehicle_type: str): + """Create the Renault integration.""" + config_entry = get_mock_config_entry() + config_entry.add_to_hass(hass) + + renault_account = RenaultAccount( + config_entry.unique_id, + websession=aiohttp_client.async_get_clientsession(hass), + ) mock_vehicle = MOCK_VEHICLES[vehicle_type] mock_fixtures = get_fixtures(vehicle_type) - vehicles_response: models.KamereonVehiclesResponse = ( - schemas.KamereonVehiclesResponseSchema.loads( - load_fixture(f"renault/vehicle_{vehicle_type}.json") - ) - ) - vehicle_details = vehicles_response.vehicleLinks[0].vehicleDetails - vehicle = RenaultVehicle( - vehicles_response.accountId, - vehicle_details.vin, - websession=aiohttp_client.async_get_clientsession(hass), - ) - - vehicle_proxy = RenaultVehicleProxy( - hass, vehicle, vehicle_details, timedelta(seconds=300) - ) - with patch( - "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.endpoint_available", + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_client.RenaultClient.get_api_account", + return_value=renault_account, + ), patch( + "renault_api.renault_account.RenaultAccount.get_vehicles", + return_value=( + schemas.KamereonVehiclesResponseSchema.loads( + load_fixture(f"renault/vehicle_{vehicle_type}.json") + ) + ), + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.supports_endpoint", side_effect=mock_vehicle["endpoints_available"], ), patch( - "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_battery_status", + "renault_api.renault_vehicle.RenaultVehicle.has_contract_for_endpoint", + return_value=True, + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_battery_status", return_value=mock_fixtures["battery_status"], ), patch( - "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_charge_mode", + "renault_api.renault_vehicle.RenaultVehicle.get_charge_mode", return_value=mock_fixtures["charge_mode"], ), patch( - "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_cockpit", + "renault_api.renault_vehicle.RenaultVehicle.get_cockpit", return_value=mock_fixtures["cockpit"], ), patch( - "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_hvac_status", + "renault_api.renault_vehicle.RenaultVehicle.get_hvac_status", return_value=mock_fixtures["hvac_status"], ): - await vehicle_proxy.async_initialise() - return vehicle_proxy + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry -async def create_vehicle_proxy_with_side_effect( +async def setup_renault_integration_vehicle_with_side_effect( hass: HomeAssistant, vehicle_type: str, side_effect: Any -) -> RenaultVehicleProxy: - """Create a vehicle proxy for testing unavailable entities.""" - mock_vehicle = MOCK_VEHICLES[vehicle_type] +): + """Create the Renault integration.""" + config_entry = get_mock_config_entry() + config_entry.add_to_hass(hass) - vehicles_response: models.KamereonVehiclesResponse = ( - schemas.KamereonVehiclesResponseSchema.loads( - load_fixture(f"renault/vehicle_{vehicle_type}.json") - ) - ) - vehicle_details = vehicles_response.vehicleLinks[0].vehicleDetails - vehicle = RenaultVehicle( - vehicles_response.accountId, - vehicle_details.vin, + renault_account = RenaultAccount( + config_entry.unique_id, websession=aiohttp_client.async_get_clientsession(hass), ) + mock_vehicle = MOCK_VEHICLES[vehicle_type] - vehicle_proxy = RenaultVehicleProxy( - hass, vehicle, vehicle_details, timedelta(seconds=300) - ) - with patch( - "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.endpoint_available", + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_client.RenaultClient.get_api_account", + return_value=renault_account, + ), patch( + "renault_api.renault_account.RenaultAccount.get_vehicles", + return_value=( + schemas.KamereonVehiclesResponseSchema.loads( + load_fixture(f"renault/vehicle_{vehicle_type}.json") + ) + ), + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.supports_endpoint", side_effect=mock_vehicle["endpoints_available"], ), patch( - "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_battery_status", + "renault_api.renault_vehicle.RenaultVehicle.has_contract_for_endpoint", + return_value=True, + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_battery_status", side_effect=side_effect, ), patch( - "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_charge_mode", + "renault_api.renault_vehicle.RenaultVehicle.get_charge_mode", side_effect=side_effect, ), patch( - "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_cockpit", + "renault_api.renault_vehicle.RenaultVehicle.get_cockpit", side_effect=side_effect, ), patch( - "homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_hvac_status", + "renault_api.renault_vehicle.RenaultVehicle.get_hvac_status", side_effect=side_effect, ): - await vehicle_proxy.async_initialise() - return vehicle_proxy + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry diff --git a/tests/components/renault/test_config_flow.py b/tests/components/renault/test_config_flow.py index c8b9c8c3e12..684e17a0101 100644 --- a/tests/components/renault/test_config_flow.py +++ b/tests/components/renault/test_config_flow.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, PropertyMock, patch from renault_api.gigya.exceptions import InvalidCredentialsException from renault_api.kamereon import schemas +from renault_api.renault_account import RenaultAccount from homeassistant import config_entries, data_entry_flow from homeassistant.components.renault.const import ( @@ -12,126 +13,197 @@ from homeassistant.components.renault.const import ( ) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client + +from . import get_mock_config_entry from tests.common import load_fixture async def test_config_flow_single_account(hass: HomeAssistant): """Test we get the form.""" - 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["errors"] == {} - - # Failed credentials with patch( - "renault_api.renault_session.RenaultSession.login", - side_effect=InvalidCredentialsException(403042, "invalid loginID or password"), - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_LOCALE: "fr_FR", - CONF_USERNAME: "email@test.com", - CONF_PASSWORD: "test", - }, + "homeassistant.components.renault.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + 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["errors"] == {} + + # Failed credentials + with patch( + "renault_api.renault_session.RenaultSession.login", + side_effect=InvalidCredentialsException( + 403042, "invalid loginID or password" + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LOCALE: "fr_FR", + CONF_USERNAME: "email@test.com", + CONF_PASSWORD: "test", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_credentials"} + + renault_account = AsyncMock() + type(renault_account).account_id = PropertyMock(return_value="account_id_1") + renault_account.get_vehicles.return_value = ( + schemas.KamereonVehiclesResponseSchema.loads( + load_fixture("renault/vehicle_zoe_40.json") + ) ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "invalid_credentials"} + # Account list single + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_account.RenaultAccount.account_id", return_value="123" + ), patch( + "renault_api.renault_client.RenaultClient.get_api_accounts", + return_value=[renault_account], + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LOCALE: "fr_FR", + CONF_USERNAME: "email@test.com", + CONF_PASSWORD: "test", + }, + ) - renault_account = AsyncMock() - type(renault_account).account_id = PropertyMock(return_value="account_id_1") - renault_account.get_vehicles.return_value = ( - schemas.KamereonVehiclesResponseSchema.loads( - load_fixture("renault/vehicle_zoe_40.json") - ) - ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "account_id_1" + assert result["data"][CONF_USERNAME] == "email@test.com" + assert result["data"][CONF_PASSWORD] == "test" + assert result["data"][CONF_KAMEREON_ACCOUNT_ID] == "account_id_1" + assert result["data"][CONF_LOCALE] == "fr_FR" - # Account list single - with patch("renault_api.renault_session.RenaultSession.login"), patch( - "renault_api.renault_account.RenaultAccount.account_id", return_value="123" - ), patch( - "renault_api.renault_client.RenaultClient.get_api_accounts", - return_value=[renault_account], - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_LOCALE: "fr_FR", - CONF_USERNAME: "email@test.com", - CONF_PASSWORD: "test", - }, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "account_id_1" - assert result["data"][CONF_USERNAME] == "email@test.com" - assert result["data"][CONF_PASSWORD] == "test" - assert result["data"][CONF_KAMEREON_ACCOUNT_ID] == "account_id_1" - assert result["data"][CONF_LOCALE] == "fr_FR" + assert len(mock_setup_entry.mock_calls) == 1 async def test_config_flow_no_account(hass: HomeAssistant): """Test we get the form.""" - 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["errors"] == {} - - # Account list empty - with patch("renault_api.renault_session.RenaultSession.login"), patch( - "homeassistant.components.renault.config_flow.RenaultHub.get_account_ids", - return_value=[], - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_LOCALE: "fr_FR", - CONF_USERNAME: "email@test.com", - CONF_PASSWORD: "test", - }, + with patch( + "homeassistant.components.renault.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + 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["errors"] == {} - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "kamereon_no_account" + # Account list empty + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_client.RenaultClient.get_api_accounts", + return_value=[], + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LOCALE: "fr_FR", + CONF_USERNAME: "email@test.com", + CONF_PASSWORD: "test", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "kamereon_no_account" + + assert len(mock_setup_entry.mock_calls) == 0 async def test_config_flow_multiple_accounts(hass: HomeAssistant): """Test what happens if multiple Kamereon accounts are available.""" - 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["errors"] == {} + with patch( + "homeassistant.components.renault.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + 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["errors"] == {} - # Multiple accounts - with patch("renault_api.renault_session.RenaultSession.login"), patch( - "homeassistant.components.renault.config_flow.RenaultHub.get_account_ids", - return_value=["account_id_1", "account_id_2"], - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_LOCALE: "fr_FR", - CONF_USERNAME: "email@test.com", - CONF_PASSWORD: "test", - }, + renault_account_1 = RenaultAccount( + "account_id_1", + websession=aiohttp_client.async_get_clientsession(hass), + ) + renault_account_2 = RenaultAccount( + "account_id_2", + websession=aiohttp_client.async_get_clientsession(hass), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "kamereon" + # Multiple accounts + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_client.RenaultClient.get_api_accounts", + return_value=[renault_account_1, renault_account_2], + ), patch("renault_api.renault_account.RenaultAccount.get_vehicles"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LOCALE: "fr_FR", + CONF_USERNAME: "email@test.com", + CONF_PASSWORD: "test", + }, + ) - # Account selected - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_KAMEREON_ACCOUNT_ID: "account_id_2"}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "account_id_2" - assert result["data"][CONF_USERNAME] == "email@test.com" - assert result["data"][CONF_PASSWORD] == "test" - assert result["data"][CONF_KAMEREON_ACCOUNT_ID] == "account_id_2" - assert result["data"][CONF_LOCALE] == "fr_FR" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "kamereon" + + # Account selected + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_KAMEREON_ACCOUNT_ID: "account_id_2"}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "account_id_2" + assert result["data"][CONF_USERNAME] == "email@test.com" + assert result["data"][CONF_PASSWORD] == "test" + assert result["data"][CONF_KAMEREON_ACCOUNT_ID] == "account_id_2" + assert result["data"][CONF_LOCALE] == "fr_FR" + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_config_flow_duplicate(hass: HomeAssistant): + """Test abort if unique_id configured.""" + with patch( + "homeassistant.components.renault.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + get_mock_config_entry().add_to_hass(hass) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + 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["errors"] == {} + + renault_account = RenaultAccount( + "account_id_1", + websession=aiohttp_client.async_get_clientsession(hass), + ) + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_client.RenaultClient.get_api_accounts", + return_value=[renault_account], + ), patch("renault_api.renault_account.RenaultAccount.get_vehicles"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LOCALE: "fr_FR", + CONF_USERNAME: "email@test.com", + CONF_PASSWORD: "test", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + await hass.async_block_till_done() + + assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/renault/test_init.py b/tests/components/renault/test_init.py index 974155c3df9..fab5eff8f0c 100644 --- a/tests/components/renault/test_init.py +++ b/tests/components/renault/test_init.py @@ -1,85 +1,63 @@ """Tests for Renault setup process.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import patch import aiohttp -import pytest from renault_api.gigya.exceptions import InvalidCredentialsException -from renault_api.kamereon import schemas -from homeassistant.components.renault import ( - RenaultHub, - async_setup_entry, - async_unload_entry, -) from homeassistant.components.renault.const import DOMAIN -from homeassistant.components.renault.renault_vehicle import RenaultVehicleProxy -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.config_entries import ConfigEntryState -from .const import MOCK_CONFIG - -from tests.common import MockConfigEntry, load_fixture +from . import get_mock_config_entry, setup_renault_integration_simple -async def test_setup_unload_and_reload_entry(hass): +async def test_setup_unload_entry(hass): """Test entry setup and unload.""" - # Create a mock entry so we don't have to go through config flow - config_entry = MockConfigEntry( - domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=123456 - ) - renault_account = AsyncMock() - renault_account.get_vehicles.return_value = ( - schemas.KamereonVehiclesResponseSchema.loads( - load_fixture("renault/vehicle_zoe_40.json") - ) - ) + with patch("homeassistant.components.renault.PLATFORMS", []): + config_entry = await setup_renault_integration_simple(hass) - with patch("renault_api.renault_session.RenaultSession.login"), patch( - "renault_api.renault_client.RenaultClient.get_api_account", - return_value=renault_account, - ): - # Set up the entry and assert that the values set during setup are where we expect - # them to be. - assert await async_setup_entry(hass, config_entry) - assert DOMAIN in hass.data and config_entry.unique_id in hass.data[DOMAIN] - assert isinstance(hass.data[DOMAIN][config_entry.unique_id], RenaultHub) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry.state is ConfigEntryState.LOADED + assert config_entry.unique_id in hass.data[DOMAIN] - renault_hub: RenaultHub = hass.data[DOMAIN][config_entry.unique_id] - assert len(renault_hub.vehicles) == 1 - assert isinstance( - renault_hub.vehicles["VF1AAAAA555777999"], RenaultVehicleProxy - ) - - # Unload the entry and verify that the data has been removed - assert await async_unload_entry(hass, config_entry) - assert config_entry.unique_id not in hass.data[DOMAIN] + # Unload the entry and verify that the data has been removed + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.NOT_LOADED + assert config_entry.unique_id not in hass.data[DOMAIN] async def test_setup_entry_bad_password(hass): """Test entry setup and unload.""" # Create a mock entry so we don't have to go through config flow - config_entry = MockConfigEntry( - domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=123456 - ) + config_entry = get_mock_config_entry() + config_entry.add_to_hass(hass) with patch( "renault_api.renault_session.RenaultSession.login", side_effect=InvalidCredentialsException(403042, "invalid loginID or password"), ): - # Set up the entry and assert that the values set during setup are where we expect - # them to be. - assert not await async_setup_entry(hass, config_entry) + 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_ERROR + assert not hass.data.get(DOMAIN) async def test_setup_entry_exception(hass): """Test ConfigEntryNotReady when API raises an exception during entry setup.""" - config_entry = MockConfigEntry( - domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=123456 - ) + config_entry = get_mock_config_entry() + config_entry.add_to_hass(hass) # In this case we are testing the condition where async_setup_entry raises # ConfigEntryNotReady. with patch( "renault_api.renault_session.RenaultSession.login", side_effect=aiohttp.ClientConnectionError, - ), pytest.raises(ConfigEntryNotReady): - assert await async_setup_entry(hass, config_entry) + ): + 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) diff --git a/tests/components/renault/test_sensor.py b/tests/components/renault/test_sensor.py index 8956fa7e7e6..01db9ac8bba 100644 --- a/tests/components/renault/test_sensor.py +++ b/tests/components/renault/test_sensor.py @@ -1,5 +1,5 @@ """Tests for Renault sensors.""" -from unittest.mock import PropertyMock, patch +from unittest.mock import patch import pytest from renault_api.kamereon import exceptions @@ -9,9 +9,8 @@ from homeassistant.const import STATE_UNAVAILABLE from homeassistant.setup import async_setup_component from . import ( - create_vehicle_proxy, - create_vehicle_proxy_with_side_effect, - setup_renault_integration, + setup_renault_integration_vehicle, + setup_renault_integration_vehicle_with_side_effect, ) from .const import MOCK_VEHICLES @@ -25,16 +24,8 @@ async def test_sensors(hass, vehicle_type): entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - vehicle_proxy = await create_vehicle_proxy(hass, vehicle_type) - - with patch( - "homeassistant.components.renault.RenaultHub.vehicles", - new_callable=PropertyMock, - return_value={ - vehicle_proxy.details.vin: vehicle_proxy, - }, - ), patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): - await setup_renault_integration(hass) + with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): + await setup_renault_integration_vehicle(hass, vehicle_type) await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] @@ -68,16 +59,8 @@ async def test_sensor_empty(hass, vehicle_type): entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - vehicle_proxy = await create_vehicle_proxy_with_side_effect(hass, vehicle_type, {}) - - with patch( - "homeassistant.components.renault.RenaultHub.vehicles", - new_callable=PropertyMock, - return_value={ - vehicle_proxy.details.vin: vehicle_proxy, - }, - ), patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): - await setup_renault_integration(hass) + with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): + await setup_renault_integration_vehicle_with_side_effect(hass, vehicle_type, {}) await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] @@ -116,18 +99,10 @@ async def test_sensor_errors(hass, vehicle_type): "Invalid response from the upstream server (The request sent to the GDC is erroneous) ; 502 Bad Gateway", ) - vehicle_proxy = await create_vehicle_proxy_with_side_effect( - hass, vehicle_type, invalid_upstream_exception - ) - - with patch( - "homeassistant.components.renault.RenaultHub.vehicles", - new_callable=PropertyMock, - return_value={ - vehicle_proxy.details.vin: vehicle_proxy, - }, - ), patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): - await setup_renault_integration(hass) + with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): + await setup_renault_integration_vehicle_with_side_effect( + hass, vehicle_type, invalid_upstream_exception + ) await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] @@ -165,18 +140,10 @@ async def test_sensor_access_denied(hass): "Access is denied for this resource", ) - vehicle_proxy = await create_vehicle_proxy_with_side_effect( - hass, "zoe_40", access_denied_exception - ) - - with patch( - "homeassistant.components.renault.RenaultHub.vehicles", - new_callable=PropertyMock, - return_value={ - vehicle_proxy.details.vin: vehicle_proxy, - }, - ), patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): - await setup_renault_integration(hass) + with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): + await setup_renault_integration_vehicle_with_side_effect( + hass, "zoe_40", access_denied_exception + ) await hass.async_block_till_done() assert len(device_registry.devices) == 0 @@ -194,18 +161,10 @@ async def test_sensor_not_supported(hass): "This feature is not technically supported by this gateway", ) - vehicle_proxy = await create_vehicle_proxy_with_side_effect( - hass, "zoe_40", not_supported_exception - ) - - with patch( - "homeassistant.components.renault.RenaultHub.vehicles", - new_callable=PropertyMock, - return_value={ - vehicle_proxy.details.vin: vehicle_proxy, - }, - ), patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): - await setup_renault_integration(hass) + with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): + await setup_renault_integration_vehicle_with_side_effect( + hass, "zoe_40", not_supported_exception + ) await hass.async_block_till_done() assert len(device_registry.devices) == 0 From d11c58dac83c375a4907ebc7f7c0e8be44a83fb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Mon, 16 Aug 2021 12:56:10 +0200 Subject: [PATCH 220/355] Improve Tractive (#54532) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Tractive, code improve Signed-off-by: Daniel Hjelseth Høyer * Tractive, code improve Signed-off-by: Daniel Hjelseth Høyer * Tractive, code improve Signed-off-by: Daniel Hjelseth Høyer * Update homeassistant/components/tractive/config_flow.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/tractive/const.py Co-authored-by: Martin Hjelmare * Tractive, comments Signed-off-by: Daniel Hjelseth Høyer * Update homeassistant/components/tractive/config_flow.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/tractive/config_flow.py Co-authored-by: Martin Hjelmare * Tractive Signed-off-by: Daniel Hjelseth Høyer * Reauth Signed-off-by: Daniel Hjelseth Høyer * Reauth Signed-off-by: Daniel Hjelseth Høyer * add tests Signed-off-by: Daniel Hjelseth Høyer * add tests Signed-off-by: Daniel Hjelseth Høyer Co-authored-by: Martin Hjelmare --- homeassistant/components/tractive/__init__.py | 5 +- .../components/tractive/config_flow.py | 42 ++++- homeassistant/components/tractive/const.py | 6 +- .../components/tractive/device_tracker.py | 52 +++--- .../components/tractive/strings.json | 4 +- tests/components/tractive/test_config_flow.py | 164 ++++++++++++++++++ 6 files changed, 237 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index cb8eff1c8bb..78ee4c7ed97 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -9,7 +9,7 @@ import aiotractive from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -38,6 +38,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) try: creds = await client.authenticate() + except aiotractive.exceptions.UnauthorizedError as error: + await client.close() + raise ConfigEntryAuthFailed from error except aiotractive.exceptions.TractiveError as error: await client.close() raise ConfigEntryNotReady from error diff --git a/homeassistant/components/tractive/config_flow.py b/homeassistant/components/tractive/config_flow.py index 70ed9071c7b..4b1fc241110 100644 --- a/homeassistant/components/tractive/config_flow.py +++ b/homeassistant/components/tractive/config_flow.py @@ -17,7 +17,9 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -STEP_USER_DATA_SCHEMA = vol.Schema({CONF_EMAIL: str, CONF_PASSWORD: str}) +USER_DATA_SCHEMA = vol.Schema( + {vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str} +) async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: @@ -47,9 +49,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle the initial step.""" if user_input is None: - return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA - ) + return self.async_show_form(step_id="user", data_schema=USER_DATA_SCHEMA) errors = {} @@ -66,7 +66,39 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=info["title"], data=user_input) return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + step_id="user", data_schema=USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_reauth(self, _: dict[str, Any]) -> FlowResult: + """Handle configuration by re-auth.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] = None + ) -> FlowResult: + """Dialog that informs the user that reauth is required.""" + + errors = {} + + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + existing_entry = await self.async_set_unique_id(info["user_id"]) + if existing_entry: + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + return self.async_abort(reason="reauth_failed_existing") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=USER_DATA_SCHEMA, + errors=errors, ) diff --git a/homeassistant/components/tractive/const.py b/homeassistant/components/tractive/const.py index 5d265c489ff..7587fedfc4c 100644 --- a/homeassistant/components/tractive/const.py +++ b/homeassistant/components/tractive/const.py @@ -6,7 +6,7 @@ DOMAIN = "tractive" RECONNECT_INTERVAL = timedelta(seconds=10) -TRACKER_HARDWARE_STATUS_UPDATED = "tracker_hardware_status_updated" -TRACKER_POSITION_UPDATED = "tracker_position_updated" +TRACKER_HARDWARE_STATUS_UPDATED = f"{DOMAIN}_tracker_hardware_status_updated" +TRACKER_POSITION_UPDATED = f"{DOMAIN}_tracker_position_updated" -SERVER_UNAVAILABLE = "tractive_server_unavailable" +SERVER_UNAVAILABLE = f"{DOMAIN}_server_unavailable" diff --git a/homeassistant/components/tractive/device_tracker.py b/homeassistant/components/tractive/device_tracker.py index 1365faa6419..82e22139f04 100644 --- a/homeassistant/components/tractive/device_tracker.py +++ b/homeassistant/components/tractive/device_tracker.py @@ -94,52 +94,52 @@ class TractiveDeviceTracker(TrackerEntity): """Return the battery level of the device.""" return self._battery_level + @callback + def _handle_hardware_status_update(self, event): + self._battery_level = event["battery_level"] + self._attr_available = True + self.async_write_ha_state() + + @callback + def _handle_position_update(self, event): + self._latitude = event["latitude"] + self._longitude = event["longitude"] + self._accuracy = event["accuracy"] + self._attr_available = True + self.async_write_ha_state() + + @callback + def _handle_server_unavailable(self): + self._latitude = None + self._longitude = None + self._accuracy = None + self._battery_level = None + self._attr_available = False + self.async_write_ha_state() + async def async_added_to_hass(self): """Handle entity which will be added.""" - @callback - def handle_hardware_status_update(event): - self._battery_level = event["battery_level"] - self._attr_available = True - self.async_write_ha_state() - self.async_on_remove( async_dispatcher_connect( self.hass, f"{TRACKER_HARDWARE_STATUS_UPDATED}-{self._tracker_id}", - handle_hardware_status_update, + self._handle_hardware_status_update, ) ) - @callback - def handle_position_update(event): - self._latitude = event["latitude"] - self._longitude = event["longitude"] - self._accuracy = event["accuracy"] - self._attr_available = True - self.async_write_ha_state() - self.async_on_remove( async_dispatcher_connect( self.hass, f"{TRACKER_POSITION_UPDATED}-{self._tracker_id}", - handle_position_update, + self._handle_position_update, ) ) - @callback - def handle_server_unavailable(): - self._latitude = None - self._longitude = None - self._accuracy = None - self._battery_level = None - self._attr_available = False - self.async_write_ha_state() - self.async_on_remove( async_dispatcher_connect( self.hass, f"{SERVER_UNAVAILABLE}-{self._user_id}", - handle_server_unavailable, + self._handle_server_unavailable, ) ) diff --git a/homeassistant/components/tractive/strings.json b/homeassistant/components/tractive/strings.json index 510b5697e56..9711eb41489 100644 --- a/homeassistant/components/tractive/strings.json +++ b/homeassistant/components/tractive/strings.json @@ -13,7 +13,9 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reauth_failed_existing": "Could not update the config entry, please remove the integration and set it up again." } } } \ No newline at end of file diff --git a/tests/components/tractive/test_config_flow.py b/tests/components/tractive/test_config_flow.py index 080aadb2bc7..7ccfdc63a34 100644 --- a/tests/components/tractive/test_config_flow.py +++ b/tests/components/tractive/test_config_flow.py @@ -7,6 +7,8 @@ from homeassistant import config_entries, setup from homeassistant.components.tractive.const import DOMAIN from homeassistant.core import HomeAssistant +from tests.common import MockConfigEntry + USER_INPUT = { "email": "test-email@example.com", "password": "test-password", @@ -76,3 +78,165 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: assert result2["type"] == "form" assert result2["errors"] == {"base": "unknown"} + + +async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: + """Test user input for config_entry that already exists.""" + first_entry = MockConfigEntry( + domain="tractive", + data=USER_INPUT, + unique_id="USERID", + ) + first_entry.add_to_hass(hass) + + with patch("aiotractive.api.API.user_id", return_value="USERID"): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_reauthentication(hass): + """Test Tractive reauthentication.""" + old_entry = MockConfigEntry( + domain="tractive", + data=USER_INPUT, + unique_id="USERID", + ) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, + ) + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "reauth_confirm" + + with patch("aiotractive.api.API.user_id", return_value="USERID"): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + + +async def test_reauthentication_failure(hass): + """Test Tractive reauthentication failure.""" + old_entry = MockConfigEntry( + domain="tractive", + data=USER_INPUT, + unique_id="USERID", + ) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, + ) + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "reauth_confirm" + + with patch( + "aiotractive.api.API.user_id", + side_effect=aiotractive.exceptions.UnauthorizedError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["step_id"] == "reauth_confirm" + assert result["type"] == "form" + assert result2["errors"]["base"] == "invalid_auth" + + +async def test_reauthentication_unknown_failure(hass): + """Test Tractive reauthentication failure.""" + old_entry = MockConfigEntry( + domain="tractive", + data=USER_INPUT, + unique_id="USERID", + ) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, + ) + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "reauth_confirm" + + with patch( + "aiotractive.api.API.user_id", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["step_id"] == "reauth_confirm" + assert result["type"] == "form" + assert result2["errors"]["base"] == "unknown" + + +async def test_reauthentication_failure_no_existing_entry(hass): + """Test Tractive reauthentication with no existing entry.""" + old_entry = MockConfigEntry( + domain="tractive", + data=USER_INPUT, + unique_id="USERID", + ) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, + ) + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "reauth_confirm" + + with patch("aiotractive.api.API.user_id", return_value="USERID_DIFFERENT"): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_failed_existing" From 2e56f66518d7e63cf3e4847bcf5a6ce95679c698 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Mon, 16 Aug 2021 04:18:19 -0700 Subject: [PATCH 221/355] Bump adb-shell to 0.4.0 (#54575) --- homeassistant/components/androidtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index d1e379435a0..00be4fa50c4 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,7 +3,7 @@ "name": "Android TV", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell[async]==0.3.4", + "adb-shell[async]==0.4.0", "androidtv[async]==0.0.60", "pure-python-adb[async]==0.3.0.dev0" ], diff --git a/requirements_all.txt b/requirements_all.txt index 3483f26245b..d6b4d721240 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -109,7 +109,7 @@ adafruit-circuitpython-mcp230xx==2.2.2 adax==0.1.1 # homeassistant.components.androidtv -adb-shell[async]==0.3.4 +adb-shell[async]==0.4.0 # homeassistant.components.alarmdecoder adext==0.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc0d8482421..aee9c071350 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -51,7 +51,7 @@ accuweather==0.2.0 adax==0.1.1 # homeassistant.components.androidtv -adb-shell[async]==0.3.4 +adb-shell[async]==0.4.0 # homeassistant.components.alarmdecoder adext==0.4.2 From a204d7f807a9fe80ca300ba37af4d1065f69026f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 16 Aug 2021 13:49:04 +0200 Subject: [PATCH 222/355] Renault code quality improvements (#53680) --- homeassistant/components/renault/__init__.py | 4 +- .../components/renault/renault_hub.py | 39 +++++++--- .../components/renault/renault_vehicle.py | 4 +- homeassistant/components/renault/sensor.py | 77 ++++++++----------- tests/components/renault/__init__.py | 59 ++++++++++++-- tests/components/renault/const.py | 29 ++++++- tests/components/renault/test_init.py | 4 +- tests/components/renault/test_sensor.py | 7 +- tests/fixtures/renault/no_data.json | 7 ++ 9 files changed, 159 insertions(+), 71 deletions(-) create mode 100644 tests/fixtures/renault/no_data.json diff --git a/homeassistant/components/renault/__init__.py b/homeassistant/components/renault/__init__.py index 80433b2106e..d4c065e52ca 100644 --- a/homeassistant/components/renault/__init__.py +++ b/homeassistant/components/renault/__init__.py @@ -26,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {}) await renault_hub.async_initialise(config_entry) - hass.data[DOMAIN][config_entry.unique_id] = renault_hub + hass.data[DOMAIN][config_entry.entry_id] = renault_hub hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) @@ -40,6 +40,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> ) if unload_ok: - hass.data[DOMAIN].pop(config_entry.unique_id) + hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok diff --git a/homeassistant/components/renault/renault_hub.py b/homeassistant/components/renault/renault_hub.py index 51e356934bb..07770ad3769 100644 --- a/homeassistant/components/renault/renault_hub.py +++ b/homeassistant/components/renault/renault_hub.py @@ -1,10 +1,12 @@ """Proxy to handle account communication with Renault servers.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging from renault_api.gigya.exceptions import InvalidCredentialsException +from renault_api.kamereon.models import KamereonVehiclesLink from renault_api.renault_account import RenaultAccount from renault_api.renault_client import RenaultClient @@ -23,7 +25,6 @@ class RenaultHub: def __init__(self, hass: HomeAssistant, locale: str) -> None: """Initialise proxy.""" - LOGGER.debug("Creating RenaultHub") self._hass = hass self._client = RenaultClient( websession=async_get_clientsession(self._hass), locale=locale @@ -49,17 +50,33 @@ class RenaultHub: self._account = await self._client.get_api_account(account_id) vehicles = await self._account.get_vehicles() if vehicles.vehicleLinks: - for vehicle_link in vehicles.vehicleLinks: - if vehicle_link.vin and vehicle_link.vehicleDetails: - # Generate vehicle proxy - vehicle = RenaultVehicleProxy( - hass=self._hass, - vehicle=await self._account.get_api_vehicle(vehicle_link.vin), - details=vehicle_link.vehicleDetails, - scan_interval=scan_interval, + await asyncio.gather( + *( + self.async_initialise_vehicle( + vehicle_link, self._account, scan_interval ) - await vehicle.async_initialise() - self._vehicles[vehicle_link.vin] = vehicle + for vehicle_link in vehicles.vehicleLinks + ) + ) + + async def async_initialise_vehicle( + self, + vehicle_link: KamereonVehiclesLink, + renault_account: RenaultAccount, + scan_interval: timedelta, + ) -> None: + """Set up proxy.""" + assert vehicle_link.vin is not None + assert vehicle_link.vehicleDetails is not None + # Generate vehicle proxy + vehicle = RenaultVehicleProxy( + hass=self._hass, + vehicle=await renault_account.get_api_vehicle(vehicle_link.vin), + details=vehicle_link.vehicleDetails, + scan_interval=scan_interval, + ) + await vehicle.async_initialise() + self._vehicles[vehicle_link.vin] = vehicle async def get_account_ids(self) -> list[str]: """Get Kamereon account ids.""" diff --git a/homeassistant/components/renault/renault_vehicle.py b/homeassistant/components/renault/renault_vehicle.py index d3f6b6e48be..8d4cfea53ee 100644 --- a/homeassistant/components/renault/renault_vehicle.py +++ b/homeassistant/components/renault/renault_vehicle.py @@ -115,7 +115,7 @@ class RenaultVehicleProxy: coordinator = self.coordinators[key] if coordinator.not_supported: # Remove endpoint as it is not supported for this vehicle. - LOGGER.error( + LOGGER.warning( "Ignoring endpoint %s as it is not supported for this vehicle: %s", coordinator.name, coordinator.last_exception, @@ -123,7 +123,7 @@ class RenaultVehicleProxy: del self.coordinators[key] elif coordinator.access_denied: # Remove endpoint as it is denied for this vehicle. - LOGGER.error( + LOGGER.warning( "Ignoring endpoint %s as it is denied for this vehicle: %s", coordinator.name, coordinator.last_exception, diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index 51f38d6a4d6..7ef11fb2afc 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -1,14 +1,14 @@ """Support for Renault sensors.""" from __future__ import annotations -from typing import Any - from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, LENGTH_KILOMETERS, PERCENTAGE, POWER_KILO_WATT, @@ -18,8 +18,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.icon import icon_for_battery_level -from homeassistant.util import slugify from .const import ( DEVICE_CLASS_CHARGE_MODE, @@ -46,20 +44,20 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Renault entities from config entry.""" - proxy: RenaultHub = hass.data[DOMAIN][config_entry.unique_id] - entities = await get_entities(proxy) + proxy: RenaultHub = hass.data[DOMAIN][config_entry.entry_id] + entities = get_entities(proxy) async_add_entities(entities) -async def get_entities(proxy: RenaultHub) -> list[RenaultDataEntity]: +def get_entities(proxy: RenaultHub) -> list[RenaultDataEntity]: """Create Renault entities for all vehicles.""" entities = [] for vehicle in proxy.vehicles.values(): - entities.extend(await get_vehicle_entities(vehicle)) + entities.extend(get_vehicle_entities(vehicle)) return entities -async def get_vehicle_entities(vehicle: RenaultVehicleProxy) -> list[RenaultDataEntity]: +def get_vehicle_entities(vehicle: RenaultVehicleProxy) -> list[RenaultDataEntity]: """Create Renault entities for single vehicle.""" entities: list[RenaultDataEntity] = [] if "cockpit" in vehicle.coordinators: @@ -78,6 +76,9 @@ async def get_vehicle_entities(vehicle: RenaultVehicleProxy) -> list[RenaultData entities.append(RenaultChargingPowerSensor(vehicle, "Charging Power")) entities.append(RenaultPlugStateSensor(vehicle, "Plug State")) entities.append(RenaultBatteryAutonomySensor(vehicle, "Battery Autonomy")) + entities.append( + RenaultBatteryAvailableEnergySensor(vehicle, "Battery Available Energy") + ) entities.append(RenaultBatteryTemperatureSensor(vehicle, "Battery Temperature")) if "charge_mode" in vehicle.coordinators: entities.append(RenaultChargeModeSensor(vehicle, "Charge Mode")) @@ -96,6 +97,18 @@ class RenaultBatteryAutonomySensor(RenaultBatteryDataEntity, SensorEntity): return self.data.batteryAutonomy if self.data else None +class RenaultBatteryAvailableEnergySensor(RenaultBatteryDataEntity, SensorEntity): + """Battery available energy sensor.""" + + _attr_device_class = DEVICE_CLASS_ENERGY + _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR + + @property + def native_value(self) -> float | None: + """Return the state of this entity.""" + return self.data.batteryAvailableEnergy if self.data else None + + class RenaultBatteryLevelSensor(RenaultBatteryDataEntity, SensorEntity): """Battery Level sensor.""" @@ -107,22 +120,6 @@ class RenaultBatteryLevelSensor(RenaultBatteryDataEntity, SensorEntity): """Return the state of this entity.""" return self.data.batteryLevel if self.data else None - @property - def icon(self) -> str: - """Icon handling.""" - return icon_for_battery_level( - battery_level=self.state, charging=self.is_charging - ) - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the state attributes of this entity.""" - attrs = super().extra_state_attributes - attrs[ATTR_BATTERY_AVAILABLE_ENERGY] = ( - self.data.batteryAvailableEnergy if self.data else None - ) - return attrs - class RenaultBatteryTemperatureSensor(RenaultBatteryDataEntity, SensorEntity): """Battery Temperature sensor.""" @@ -163,7 +160,7 @@ class RenaultChargeStateSensor(RenaultBatteryDataEntity, SensorEntity): def native_value(self) -> str | None: """Return the state of this entity.""" charging_status = self.data.get_charging_status() if self.data else None - return slugify(charging_status.name) if charging_status is not None else None + return charging_status.name.lower() if charging_status is not None else None @property def icon(self) -> str: @@ -186,7 +183,7 @@ class RenaultChargingRemainingTimeSensor(RenaultBatteryDataEntity, SensorEntity) class RenaultChargingPowerSensor(RenaultBatteryDataEntity, SensorEntity): """Charging Power sensor.""" - _attr_device_class = DEVICE_CLASS_ENERGY + _attr_device_class = DEVICE_CLASS_POWER _attr_native_unit_of_measurement = POWER_KILO_WATT @property @@ -209,11 +206,9 @@ class RenaultFuelAutonomySensor(RenaultCockpitDataEntity, SensorEntity): @property def native_value(self) -> int | None: """Return the state of this entity.""" - return ( - round(self.data.fuelAutonomy) - if self.data and self.data.fuelAutonomy is not None - else None - ) + if not self.data or self.data.fuelAutonomy is None: + return None + return round(self.data.fuelAutonomy) class RenaultFuelQuantitySensor(RenaultCockpitDataEntity, SensorEntity): @@ -225,11 +220,9 @@ class RenaultFuelQuantitySensor(RenaultCockpitDataEntity, SensorEntity): @property def native_value(self) -> int | None: """Return the state of this entity.""" - return ( - round(self.data.fuelQuantity) - if self.data and self.data.fuelQuantity is not None - else None - ) + if not self.data or self.data.fuelQuantity is None: + return None + return round(self.data.fuelQuantity) class RenaultMileageSensor(RenaultCockpitDataEntity, SensorEntity): @@ -241,11 +234,9 @@ class RenaultMileageSensor(RenaultCockpitDataEntity, SensorEntity): @property def native_value(self) -> int | None: """Return the state of this entity.""" - return ( - round(self.data.totalMileage) - if self.data and self.data.totalMileage is not None - else None - ) + if not self.data or self.data.totalMileage is None: + return None + return round(self.data.totalMileage) class RenaultOutsideTemperatureSensor(RenaultHVACDataEntity, SensorEntity): @@ -269,7 +260,7 @@ class RenaultPlugStateSensor(RenaultBatteryDataEntity, SensorEntity): def native_value(self) -> str | None: """Return the state of this entity.""" plug_status = self.data.get_plug_status() if self.data else None - return slugify(plug_status.name) if plug_status is not None else None + return plug_status.name.lower() if plug_status is not None else None @property def icon(self) -> str: diff --git a/tests/components/renault/__init__.py b/tests/components/renault/__init__.py index fcd190fe98d..da72da05d5d 100644 --- a/tests/components/renault/__init__.py +++ b/tests/components/renault/__init__.py @@ -31,27 +31,27 @@ def get_mock_config_entry(): def get_fixtures(vehicle_type: str) -> dict[str, Any]: """Create a vehicle proxy for testing.""" - mock_vehicle = MOCK_VEHICLES[vehicle_type] + mock_vehicle = MOCK_VEHICLES.get(vehicle_type, {"endpoints": {}}) return { "battery_status": schemas.KamereonVehicleDataResponseSchema.loads( load_fixture(f"renault/{mock_vehicle['endpoints']['battery_status']}") if "battery_status" in mock_vehicle["endpoints"] - else "{}" + else load_fixture("renault/no_data.json") ).get_attributes(schemas.KamereonVehicleBatteryStatusDataSchema), "charge_mode": schemas.KamereonVehicleDataResponseSchema.loads( load_fixture(f"renault/{mock_vehicle['endpoints']['charge_mode']}") if "charge_mode" in mock_vehicle["endpoints"] - else "{}" + else load_fixture("renault/no_data.json") ).get_attributes(schemas.KamereonVehicleChargeModeDataSchema), "cockpit": schemas.KamereonVehicleDataResponseSchema.loads( load_fixture(f"renault/{mock_vehicle['endpoints']['cockpit']}") if "cockpit" in mock_vehicle["endpoints"] - else "{}" + else load_fixture("renault/no_data.json") ).get_attributes(schemas.KamereonVehicleCockpitDataSchema), "hvac_status": schemas.KamereonVehicleDataResponseSchema.loads( load_fixture(f"renault/{mock_vehicle['endpoints']['hvac_status']}") if "hvac_status" in mock_vehicle["endpoints"] - else "{}" + else load_fixture("renault/no_data.json") ).get_attributes(schemas.KamereonVehicleHvacStatusDataSchema), } @@ -123,6 +123,55 @@ async def setup_renault_integration_vehicle(hass: HomeAssistant, vehicle_type: s return config_entry +async def setup_renault_integration_vehicle_with_no_data( + hass: HomeAssistant, vehicle_type: str +): + """Create the Renault integration.""" + config_entry = get_mock_config_entry() + config_entry.add_to_hass(hass) + + renault_account = RenaultAccount( + config_entry.unique_id, + websession=aiohttp_client.async_get_clientsession(hass), + ) + mock_vehicle = MOCK_VEHICLES[vehicle_type] + mock_fixtures = get_fixtures("") + + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_client.RenaultClient.get_api_account", + return_value=renault_account, + ), patch( + "renault_api.renault_account.RenaultAccount.get_vehicles", + return_value=( + schemas.KamereonVehiclesResponseSchema.loads( + load_fixture(f"renault/vehicle_{vehicle_type}.json") + ) + ), + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.supports_endpoint", + side_effect=mock_vehicle["endpoints_available"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.has_contract_for_endpoint", + return_value=True, + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_battery_status", + return_value=mock_fixtures["battery_status"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_charge_mode", + return_value=mock_fixtures["charge_mode"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_cockpit", + return_value=mock_fixtures["cockpit"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_hvac_status", + return_value=mock_fixtures["hvac_status"], + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry + + async def setup_renault_integration_vehicle_with_side_effect( hass: HomeAssistant, vehicle_type: str, side_effect: Any ): diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index be2adafd7be..8c3d6e9f98f 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -13,7 +13,9 @@ from homeassistant.const import ( CONF_USERNAME, DEVICE_CLASS_BATTERY, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, LENGTH_KILOMETERS, PERCENTAGE, POWER_KILO_WATT, @@ -59,6 +61,13 @@ MOCK_VEHICLES = { "result": "141", "unit": LENGTH_KILOMETERS, }, + { + "entity_id": "sensor.battery_available_energy", + "unique_id": "vf1aaaaa555777999_battery_available_energy", + "result": "31", + "unit": ENERGY_KILO_WATT_HOUR, + "class": DEVICE_CLASS_ENERGY, + }, { "entity_id": "sensor.battery_level", "unique_id": "vf1aaaaa555777999_battery_level", @@ -90,7 +99,7 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_charging_power", "result": "0.027", "unit": POWER_KILO_WATT, - "class": DEVICE_CLASS_ENERGY, + "class": DEVICE_CLASS_POWER, }, { "entity_id": "sensor.charging_remaining_time", @@ -145,6 +154,13 @@ MOCK_VEHICLES = { "result": "128", "unit": LENGTH_KILOMETERS, }, + { + "entity_id": "sensor.battery_available_energy", + "unique_id": "vf1aaaaa555777999_battery_available_energy", + "result": "0", + "unit": ENERGY_KILO_WATT_HOUR, + "class": DEVICE_CLASS_ENERGY, + }, { "entity_id": "sensor.battery_level", "unique_id": "vf1aaaaa555777999_battery_level", @@ -176,7 +192,7 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_charging_power", "result": STATE_UNKNOWN, "unit": POWER_KILO_WATT, - "class": DEVICE_CLASS_ENERGY, + "class": DEVICE_CLASS_POWER, }, { "entity_id": "sensor.charging_remaining_time", @@ -224,6 +240,13 @@ MOCK_VEHICLES = { "result": "141", "unit": LENGTH_KILOMETERS, }, + { + "entity_id": "sensor.battery_available_energy", + "unique_id": "vf1aaaaa555777123_battery_available_energy", + "result": "31", + "unit": ENERGY_KILO_WATT_HOUR, + "class": DEVICE_CLASS_ENERGY, + }, { "entity_id": "sensor.battery_level", "unique_id": "vf1aaaaa555777123_battery_level", @@ -255,7 +278,7 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777123_charging_power", "result": "27.0", "unit": POWER_KILO_WATT, - "class": DEVICE_CLASS_ENERGY, + "class": DEVICE_CLASS_POWER, }, { "entity_id": "sensor.charging_remaining_time", diff --git a/tests/components/renault/test_init.py b/tests/components/renault/test_init.py index fab5eff8f0c..37a67151972 100644 --- a/tests/components/renault/test_init.py +++ b/tests/components/renault/test_init.py @@ -17,13 +17,13 @@ async def test_setup_unload_entry(hass): assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert config_entry.state is ConfigEntryState.LOADED - assert config_entry.unique_id in hass.data[DOMAIN] + assert config_entry.entry_id in hass.data[DOMAIN] # Unload the entry and verify that the data has been removed await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.NOT_LOADED - assert config_entry.unique_id not in hass.data[DOMAIN] + assert config_entry.entry_id not in hass.data[DOMAIN] async def test_setup_entry_bad_password(hass): diff --git a/tests/components/renault/test_sensor.py b/tests/components/renault/test_sensor.py index 01db9ac8bba..42a75012b38 100644 --- a/tests/components/renault/test_sensor.py +++ b/tests/components/renault/test_sensor.py @@ -5,11 +5,12 @@ import pytest from renault_api.kamereon import exceptions from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.setup import async_setup_component from . import ( setup_renault_integration_vehicle, + setup_renault_integration_vehicle_with_no_data, setup_renault_integration_vehicle_with_side_effect, ) from .const import MOCK_VEHICLES @@ -60,7 +61,7 @@ async def test_sensor_empty(hass, vehicle_type): device_registry = mock_device_registry(hass) with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): - await setup_renault_integration_vehicle_with_side_effect(hass, vehicle_type, {}) + await setup_renault_integration_vehicle_with_no_data(hass, vehicle_type) await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] @@ -84,7 +85,7 @@ async def test_sensor_empty(hass, vehicle_type): assert registry_entry.unit_of_measurement == expected_entity.get("unit") assert registry_entry.device_class == expected_entity.get("class") state = hass.states.get(entity_id) - assert state.state == STATE_UNAVAILABLE + assert state.state == STATE_UNKNOWN @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) diff --git a/tests/fixtures/renault/no_data.json b/tests/fixtures/renault/no_data.json new file mode 100644 index 00000000000..7b78844ca99 --- /dev/null +++ b/tests/fixtures/renault/no_data.json @@ -0,0 +1,7 @@ +{ + "data": { + "type": "Car", + "id": "VF1AAAAA555777999", + "attributes": {} + } +} From a7918e90ab6a913a6cb2e868b4614d0fe0358f9c Mon Sep 17 00:00:00 2001 From: Dylan Gore Date: Mon, 16 Aug 2021 12:52:40 +0100 Subject: [PATCH 223/355] Update PyMetEireann to 2021.8.0 (#54693) --- homeassistant/components/met_eireann/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/met_eireann/manifest.json b/homeassistant/components/met_eireann/manifest.json index 9d2e1857689..36cc905eabf 100644 --- a/homeassistant/components/met_eireann/manifest.json +++ b/homeassistant/components/met_eireann/manifest.json @@ -3,7 +3,7 @@ "name": "Met Éireann", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/met_eireann", - "requirements": ["pyMetEireann==0.2"], + "requirements": ["pyMetEireann==2021.8.0"], "codeowners": ["@DylanGore"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index d6b4d721240..db0885255bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1275,7 +1275,7 @@ pyControl4==0.0.6 pyHS100==0.3.5.2 # homeassistant.components.met_eireann -pyMetEireann==0.2 +pyMetEireann==2021.8.0 # homeassistant.components.met # homeassistant.components.norway_air diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aee9c071350..e2e8f737214 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -717,7 +717,7 @@ pyControl4==0.0.6 pyHS100==0.3.5.2 # homeassistant.components.met_eireann -pyMetEireann==0.2 +pyMetEireann==2021.8.0 # homeassistant.components.met # homeassistant.components.norway_air From 2bcfae6998c972b2648cc843eb3865927a032ccd Mon Sep 17 00:00:00 2001 From: serenewaffles Date: Mon, 16 Aug 2021 09:23:48 -0400 Subject: [PATCH 224/355] Fix typo in Todoist service description (#54662) --- homeassistant/components/todoist/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/todoist/services.yaml b/homeassistant/components/todoist/services.yaml index 85e975e94ff..d0b680375f9 100644 --- a/homeassistant/components/todoist/services.yaml +++ b/homeassistant/components/todoist/services.yaml @@ -30,7 +30,7 @@ new_task: min: 1 max: 4 due_date_string: - name: Dure date string + name: Due date string description: The day this task is due, in natural language. example: Tomorrow selector: From 3e93215a1fd819ffb6620de57de92a7ad7b2cb9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Bogda=C5=82?= Date: Mon, 16 Aug 2021 15:49:12 +0200 Subject: [PATCH 225/355] Fix event type names for non-specified Traccar events (#54561) * Fix event type name * Extend list of types only when all_events is specified * Remove flake8 warnings --- .../components/traccar/device_tracker.py | 44 ++++++------- .../components/traccar/test_device_tracker.py | 62 +++++++++++++++++++ 2 files changed, 85 insertions(+), 21 deletions(-) create mode 100644 tests/components/traccar/test_device_tracker.py diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index 5ad5879f31b..16cd9ba94e5 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -74,6 +74,26 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) SCAN_INTERVAL = DEFAULT_SCAN_INTERVAL +EVENTS = [ + EVENT_DEVICE_MOVING, + EVENT_COMMAND_RESULT, + EVENT_DEVICE_FUEL_DROP, + EVENT_GEOFENCE_ENTER, + EVENT_DEVICE_OFFLINE, + EVENT_DRIVER_CHANGED, + EVENT_GEOFENCE_EXIT, + EVENT_DEVICE_OVERSPEED, + EVENT_DEVICE_ONLINE, + EVENT_DEVICE_STOPPED, + EVENT_MAINTENANCE, + EVENT_ALARM, + EVENT_TEXT_MESSAGE, + EVENT_DEVICE_UNKNOWN, + EVENT_IGNITION_OFF, + EVENT_IGNITION_ON, + EVENT_ALL_EVENTS, +] + PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_PASSWORD): cv.string, @@ -91,27 +111,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ), vol.Optional(CONF_EVENT, default=[]): vol.All( cv.ensure_list, - [ - vol.Any( - EVENT_DEVICE_MOVING, - EVENT_COMMAND_RESULT, - EVENT_DEVICE_FUEL_DROP, - EVENT_GEOFENCE_ENTER, - EVENT_DEVICE_OFFLINE, - EVENT_DRIVER_CHANGED, - EVENT_GEOFENCE_EXIT, - EVENT_DEVICE_OVERSPEED, - EVENT_DEVICE_ONLINE, - EVENT_DEVICE_STOPPED, - EVENT_MAINTENANCE, - EVENT_ALARM, - EVENT_TEXT_MESSAGE, - EVENT_DEVICE_UNKNOWN, - EVENT_IGNITION_OFF, - EVENT_IGNITION_ON, - EVENT_ALL_EVENTS, - ) - ], + [vol.In(EVENTS)], ), } ) @@ -203,6 +203,8 @@ class TraccarScanner: ): """Initialize.""" + if EVENT_ALL_EVENTS in event_types: + event_types = EVENTS self._event_types = {camelcase(evt): evt for evt in event_types} self._custom_attributes = custom_attributes self._scan_interval = scan_interval diff --git a/tests/components/traccar/test_device_tracker.py b/tests/components/traccar/test_device_tracker.py new file mode 100644 index 00000000000..4e2f5e0ff09 --- /dev/null +++ b/tests/components/traccar/test_device_tracker.py @@ -0,0 +1,62 @@ +"""The tests for the Traccar device tracker platform.""" +from datetime import datetime +from unittest.mock import AsyncMock, patch + +from homeassistant.components.device_tracker.const import DOMAIN +from homeassistant.components.traccar.device_tracker import ( + PLATFORM_SCHEMA as TRACCAR_PLATFORM_SCHEMA, +) +from homeassistant.const import ( + CONF_EVENT, + CONF_HOST, + CONF_PASSWORD, + CONF_PLATFORM, + CONF_USERNAME, +) +from homeassistant.setup import async_setup_component + +from tests.common import async_capture_events + + +async def test_import_events_catch_all(hass): + """Test importing all events and firing them in HA using their event types.""" + conf_dict = { + DOMAIN: TRACCAR_PLATFORM_SCHEMA( + { + CONF_PLATFORM: "traccar", + CONF_HOST: "fake_host", + CONF_USERNAME: "fake_user", + CONF_PASSWORD: "fake_pass", + CONF_EVENT: ["all_events"], + } + ) + } + + device = {"id": 1, "name": "abc123"} + api_mock = AsyncMock() + api_mock.devices = [device] + api_mock.get_events.return_value = [ + { + "deviceId": device["id"], + "type": "ignitionOn", + "serverTime": datetime.utcnow(), + "attributes": {}, + }, + { + "deviceId": device["id"], + "type": "ignitionOff", + "serverTime": datetime.utcnow(), + "attributes": {}, + }, + ] + + events_ignition_on = async_capture_events(hass, "traccar_ignition_on") + events_ignition_off = async_capture_events(hass, "traccar_ignition_off") + + with patch( + "homeassistant.components.traccar.device_tracker.API", return_value=api_mock + ): + assert await async_setup_component(hass, DOMAIN, conf_dict) + + assert len(events_ignition_on) == 1 + assert len(events_ignition_off) == 1 From 979165669c017423535ba9ede1d5d6d943cb43f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Mon, 16 Aug 2021 15:57:23 +0200 Subject: [PATCH 226/355] Update mill to use state class total (#54581) --- homeassistant/components/mill/sensor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index a8b4554139f..5241f95abdb 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -2,7 +2,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL, SensorEntity, ) from homeassistant.const import ENERGY_KILO_WATT_HOUR @@ -27,17 +27,18 @@ async def async_setup_entry(hass, entry, async_add_entities): class MillHeaterEnergySensor(SensorEntity): """Representation of a Mill Sensor device.""" + _attr_device_class = DEVICE_CLASS_ENERGY + _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR + _attr_state_class = STATE_CLASS_TOTAL + def __init__(self, heater, mill_data_connection, sensor_type): """Initialize the sensor.""" self._id = heater.device_id self._conn = mill_data_connection self._sensor_type = sensor_type - self._attr_device_class = DEVICE_CLASS_ENERGY self._attr_name = f"{heater.name} {sensor_type.replace('_', ' ')}" self._attr_unique_id = f"{heater.device_id}_{sensor_type}" - self._attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR - self._attr_state_class = STATE_CLASS_MEASUREMENT self._attr_device_info = { "identifiers": {(DOMAIN, heater.device_id)}, "name": self.name, From 99a62799ae68cd71f77cb36c3b4c2a5cac678286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 16 Aug 2021 16:07:11 +0200 Subject: [PATCH 227/355] Allow non-admin users to call history/list_statistic_ids (#54698) --- homeassistant/components/history/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index a1e0fd45167..518e555c280 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -167,7 +167,6 @@ async def ws_get_statistics_during_period( vol.Optional("statistic_type"): vol.Any("sum", "mean"), } ) -@websocket_api.require_admin @websocket_api.async_response async def ws_get_list_statistic_ids( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict From 75a2ac08080378ad1ab8495ba5f13df2495a0e5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Mon, 16 Aug 2021 16:10:07 +0200 Subject: [PATCH 228/355] Update melcloud to use state class total increasing (#54607) --- homeassistant/components/melcloud/sensor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index 12029127b84..608c3547724 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -11,11 +11,11 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, ) from homeassistant.const import ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS -from homeassistant.util import dt as dt_util from . import MelCloudDevice from .const import DOMAIN @@ -150,10 +150,11 @@ class MelDeviceSensor(SensorEntity): self._attr_name = f"{api.name} {description.name}" self._attr_unique_id = f"{api.device.serial}-{api.device.mac}-{description.key}" - self._attr_state_class = STATE_CLASS_MEASUREMENT if description.device_class == DEVICE_CLASS_ENERGY: - self._attr_last_reset = dt_util.utc_from_timestamp(0) + self._attr_state_class = STATE_CLASS_TOTAL_INCREASING + else: + self._attr_state_class = STATE_CLASS_MEASUREMENT @property def native_value(self): From 2eba63338252f49d059ac7f70df779f908fa9b22 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 16 Aug 2021 16:13:07 +0200 Subject: [PATCH 229/355] Fix 'in' comparisons vesync light (#54614) --- homeassistant/components/vesync/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vesync/light.py b/homeassistant/components/vesync/light.py index b747c10ee4e..bd187f2f590 100644 --- a/homeassistant/components/vesync/light.py +++ b/homeassistant/components/vesync/light.py @@ -46,7 +46,7 @@ def _async_setup_entities(devices, async_add_entities): for dev in devices: if DEV_TYPE_TO_HA.get(dev.device_type) in ("walldimmer", "bulb-dimmable"): entities.append(VeSyncDimmableLightHA(dev)) - elif DEV_TYPE_TO_HA.get(dev.device_type) in ("bulb-tunable-white"): + elif DEV_TYPE_TO_HA.get(dev.device_type) in ("bulb-tunable-white",): entities.append(VeSyncTunableWhiteLightHA(dev)) else: _LOGGER.debug( @@ -82,7 +82,7 @@ class VeSyncBaseLight(VeSyncDevice, LightEntity): """Turn the device on.""" attribute_adjustment_only = False # set white temperature - if self.color_mode in (COLOR_MODE_COLOR_TEMP) and ATTR_COLOR_TEMP in kwargs: + if self.color_mode in (COLOR_MODE_COLOR_TEMP,) and ATTR_COLOR_TEMP in kwargs: # get white temperature from HA data color_temp = int(kwargs[ATTR_COLOR_TEMP]) # ensure value between min-max supported Mireds From a892605a90395c1789ea2da361b20b3fcef5365a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 16 Aug 2021 16:15:42 +0200 Subject: [PATCH 230/355] Bump pytautulli (#54594) --- .../components/tautulli/manifest.json | 2 +- homeassistant/components/tautulli/sensor.py | 97 +++++++++++++------ requirements_all.txt | 2 +- 3 files changed, 69 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/tautulli/manifest.json b/homeassistant/components/tautulli/manifest.json index cb2e38ebd6d..d413e477397 100644 --- a/homeassistant/components/tautulli/manifest.json +++ b/homeassistant/components/tautulli/manifest.json @@ -2,7 +2,7 @@ "domain": "tautulli", "name": "Tautulli", "documentation": "https://www.home-assistant.io/integrations/tautulli", - "requirements": ["pytautulli==0.5.0"], + "requirements": ["pytautulli==21.8.1"], "codeowners": ["@ludeeus"], "iot_class": "local_polling" } diff --git a/homeassistant/components/tautulli/sensor.py b/homeassistant/components/tautulli/sensor.py index 67df02cb15d..16b58b206aa 100644 --- a/homeassistant/components/tautulli/sensor.py +++ b/homeassistant/components/tautulli/sensor.py @@ -1,7 +1,7 @@ """A platform which allows you to get information from Tautulli.""" from datetime import timedelta -from pytautulli import Tautulli +from pytautulli import PyTautulli import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity @@ -60,10 +60,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= session = async_get_clientsession(hass, verify_ssl) tautulli = TautulliData( - Tautulli(host, port, api_key, hass.loop, session, use_ssl, path) + PyTautulli( + api_token=api_key, + hostname=host, + session=session, + verify_ssl=verify_ssl, + port=port, + ssl=use_ssl, + base_api_path=path, + ) ) - if not await tautulli.test_connection(): + await tautulli.async_update() + if not tautulli.activity or not tautulli.home_stats or not tautulli.users: raise PlatformNotReady sensor = [TautulliSensor(tautulli, name, monitored_conditions, user)] @@ -88,25 +97,52 @@ class TautulliSensor(SensorEntity): async def async_update(self): """Get the latest data from the Tautulli API.""" await self.tautulli.async_update() - self.home = self.tautulli.api.home_data - self.sessions = self.tautulli.api.session_data - self._attributes["Top Movie"] = self.home.get("movie") - self._attributes["Top TV Show"] = self.home.get("tv") - self._attributes["Top User"] = self.home.get("user") - for key in self.sessions: - if "sessions" not in key: - self._attributes[key] = self.sessions[key] - for user in self.tautulli.api.users: - if self.usernames is None or user in self.usernames: - userdata = self.tautulli.api.user_data - self._attributes[user] = {} - self._attributes[user]["Activity"] = userdata[user]["Activity"] - if self.monitored_conditions: - for key in self.monitored_conditions: - try: - self._attributes[user][key] = userdata[user][key] - except (KeyError, TypeError): - self._attributes[user][key] = "" + if ( + not self.tautulli.activity + or not self.tautulli.home_stats + or not self.tautulli.users + ): + return + + self._attributes = { + "stream_count": self.tautulli.activity.stream_count, + "stream_count_direct_play": self.tautulli.activity.stream_count_direct_play, + "stream_count_direct_stream": self.tautulli.activity.stream_count_direct_stream, + "stream_count_transcode": self.tautulli.activity.stream_count_transcode, + "total_bandwidth": self.tautulli.activity.total_bandwidth, + "lan_bandwidth": self.tautulli.activity.lan_bandwidth, + "wan_bandwidth": self.tautulli.activity.wan_bandwidth, + } + + for stat in self.tautulli.home_stats: + if stat.stat_id == "top_movies": + self._attributes["Top Movie"] = ( + stat.rows[0].title if stat.rows else None + ) + elif stat.stat_id == "top_tv": + self._attributes["Top TV Show"] = ( + stat.rows[0].title if stat.rows else None + ) + elif stat.stat_id == "top_users": + self._attributes["Top User"] = stat.rows[0].user if stat.rows else None + + for user in self.tautulli.users: + if ( + self.usernames + and user.username not in self.usernames + or user.username == "Local" + ): + continue + self._attributes.setdefault(user.username, {})["Activity"] = None + + for session in self.tautulli.activity.sessions: + if not self._attributes.get(session.username): + continue + + self._attributes[session.username]["Activity"] = session.state + if self.monitored_conditions: + for key in self.monitored_conditions: + self._attributes[session.username][key] = getattr(session, key) @property def name(self): @@ -116,7 +152,9 @@ class TautulliSensor(SensorEntity): @property def native_value(self): """Return the state of the sensor.""" - return self.sessions.get("stream_count") + if not self.tautulli.activity: + return 0 + return self.tautulli.activity.stream_count @property def icon(self): @@ -140,14 +178,13 @@ class TautulliData: def __init__(self, api): """Initialize the data object.""" self.api = api + self.activity = None + self.home_stats = None + self.users = None @Throttle(TIME_BETWEEN_UPDATES) async def async_update(self): """Get the latest data from Tautulli.""" - await self.api.get_data() - - async def test_connection(self): - """Test connection to Tautulli.""" - await self.api.test_connection() - connection_status = self.api.connection - return connection_status + self.activity = await self.api.async_get_activity() + self.home_stats = await self.api.async_get_home_stats() + self.users = await self.api.async_get_users() diff --git a/requirements_all.txt b/requirements_all.txt index db0885255bc..9203b216eb7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1803,7 +1803,7 @@ pysyncthru==0.7.3 pytankerkoenig==0.0.6 # homeassistant.components.tautulli -pytautulli==0.5.0 +pytautulli==21.8.1 # homeassistant.components.tfiac pytfiac==0.4 From 844000556f1493e3c6d82001e8883d9109a0ffa2 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Mon, 16 Aug 2021 16:16:36 +0200 Subject: [PATCH 231/355] Set correct ESPHome color mode when setting color temperature (#54596) --- homeassistant/components/esphome/light.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index c6cf9742082..73339769121 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -157,7 +157,11 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): if (color_temp := kwargs.get(ATTR_COLOR_TEMP)) is not None: data["color_temperature"] = color_temp if self._supports_color_mode: - data["color_mode"] = LightColorMode.COLOR_TEMPERATURE + supported_modes = self._native_supported_color_modes + if LightColorMode.COLOR_TEMPERATURE in supported_modes: + data["color_mode"] = LightColorMode.COLOR_TEMPERATURE + elif LightColorMode.COLD_WARM_WHITE in supported_modes: + data["color_mode"] = LightColorMode.COLD_WARM_WHITE if (effect := kwargs.get(ATTR_EFFECT)) is not None: data["effect"] = effect From c5d88d3e2f36fc677e589d802035c16e54f5bddd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 16 Aug 2021 16:19:41 +0200 Subject: [PATCH 232/355] Remove last_reset attribute from dsmr_reader sensors (#54700) --- .../components/dsmr_reader/definitions.py | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index a5fc2b8147a..6edf2972aa4 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -6,6 +6,7 @@ from typing import Callable from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntityDescription, ) from homeassistant.const import ( @@ -21,7 +22,6 @@ from homeassistant.const import ( POWER_KILO_WATT, VOLUME_CUBIC_METERS, ) -from homeassistant.util import dt as dt_util def dsmr_transform(value): @@ -51,32 +51,28 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Low tariff usage", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_MEASUREMENT, - last_reset=dt_util.utc_from_timestamp(0), + state_class=STATE_CLASS_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_MEASUREMENT, - last_reset=dt_util.utc_from_timestamp(0), + state_class=STATE_CLASS_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_MEASUREMENT, - last_reset=dt_util.utc_from_timestamp(0), + state_class=STATE_CLASS_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_MEASUREMENT, - last_reset=dt_util.utc_from_timestamp(0), + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/electricity_currently_delivered", @@ -146,8 +142,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, icon="mdi:fire", native_unit_of_measurement=VOLUME_CUBIC_METERS, - state_class=STATE_CLASS_MEASUREMENT, - last_reset=dt_util.utc_from_timestamp(0), + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/phase_voltage_l1", @@ -208,8 +203,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Gas usage", icon="mdi:fire", native_unit_of_measurement=VOLUME_CUBIC_METERS, - state_class=STATE_CLASS_MEASUREMENT, - last_reset=dt_util.utc_from_timestamp(0), + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/consumption/gas/currently_delivered", @@ -229,48 +223,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_MEASUREMENT, - last_reset=dt_util.utc_from_timestamp(0), + state_class=STATE_CLASS_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_MEASUREMENT, - last_reset=dt_util.utc_from_timestamp(0), + state_class=STATE_CLASS_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_MEASUREMENT, - last_reset=dt_util.utc_from_timestamp(0), + state_class=STATE_CLASS_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_MEASUREMENT, - last_reset=dt_util.utc_from_timestamp(0), + state_class=STATE_CLASS_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_MEASUREMENT, - last_reset=dt_util.utc_from_timestamp(0), + state_class=STATE_CLASS_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_MEASUREMENT, - last_reset=dt_util.utc_from_timestamp(0), + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity1_cost", From 512a474e934798c0bd2bdf5860bf5a56956045e5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 16 Aug 2021 07:28:26 -0700 Subject: [PATCH 233/355] Allow specifying discovery without a config flow (#54677) --- .../components/rainforest_eagle/manifest.json | 7 +++- script/hassfest/config_flow.py | 37 +------------------ script/hassfest/dhcp.py | 2 +- script/hassfest/model.py | 5 +++ script/hassfest/mqtt.py | 2 +- script/hassfest/ssdp.py | 2 +- script/hassfest/zeroconf.py | 2 +- 7 files changed, 16 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/rainforest_eagle/manifest.json b/homeassistant/components/rainforest_eagle/manifest.json index fd28e5b0994..4b6268fd59a 100644 --- a/homeassistant/components/rainforest_eagle/manifest.json +++ b/homeassistant/components/rainforest_eagle/manifest.json @@ -4,5 +4,10 @@ "documentation": "https://www.home-assistant.io/integrations/rainforest_eagle", "requirements": ["eagle200_reader==0.2.4", "uEagle==0.0.2"], "codeowners": ["@gtdiehl", "@jcalbert"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "dhcp": [ + { + "macaddress": "D8D5B9*" + } + ] } diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py index e4d1be7bc46..8e0f53fd736 100644 --- a/script/hassfest/config_flow.py +++ b/script/hassfest/config_flow.py @@ -29,31 +29,6 @@ def validate_integration(config: Config, integration: Integration): "config_flow", "Config flows need to be defined in the file config_flow.py", ) - if integration.manifest.get("homekit"): - integration.add_error( - "config_flow", - "HomeKit information in a manifest requires a config flow to exist", - ) - if integration.manifest.get("mqtt"): - integration.add_error( - "config_flow", - "MQTT information in a manifest requires a config flow to exist", - ) - if integration.manifest.get("ssdp"): - integration.add_error( - "config_flow", - "SSDP information in a manifest requires a config flow to exist", - ) - if integration.manifest.get("zeroconf"): - integration.add_error( - "config_flow", - "Zeroconf information in a manifest requires a config flow to exist", - ) - if integration.manifest.get("dhcp"): - integration.add_error( - "config_flow", - "DHCP information in a manifest requires a config flow to exist", - ) return config_flow = config_flow_file.read_text() @@ -98,17 +73,7 @@ def generate_and_validate(integrations: dict[str, Integration], config: Config): for domain in sorted(integrations): integration = integrations[domain] - if not integration.manifest: - continue - - if not ( - integration.manifest.get("config_flow") - or integration.manifest.get("homekit") - or integration.manifest.get("mqtt") - or integration.manifest.get("ssdp") - or integration.manifest.get("zeroconf") - or integration.manifest.get("dhcp") - ): + if not integration.manifest or not integration.config_flow: continue validate_integration(config, integration) diff --git a/script/hassfest/dhcp.py b/script/hassfest/dhcp.py index a3abe80063e..c746c64e46f 100644 --- a/script/hassfest/dhcp.py +++ b/script/hassfest/dhcp.py @@ -24,7 +24,7 @@ def generate_and_validate(integrations: list[dict[str, str]]): for domain in sorted(integrations): integration = integrations[domain] - if not integration.manifest: + if not integration.manifest or not integration.config_flow: continue match_types = integration.manifest.get("dhcp", []) diff --git a/script/hassfest/model.py b/script/hassfest/model.py index b20df6ea42f..69810686cc1 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -96,6 +96,11 @@ class Integration: """Return quality scale of the integration.""" return self.manifest.get("quality_scale") + @property + def config_flow(self) -> str: + """Return if the integration has a config flow.""" + return self.manifest.get("config_flow") + @property def requirements(self) -> list[str]: """List of requirements.""" diff --git a/script/hassfest/mqtt.py b/script/hassfest/mqtt.py index 718df4ac827..f325518d7b9 100644 --- a/script/hassfest/mqtt.py +++ b/script/hassfest/mqtt.py @@ -26,7 +26,7 @@ def generate_and_validate(integrations: dict[str, Integration]): for domain in sorted(integrations): integration = integrations[domain] - if not integration.manifest: + if not integration.manifest or not integration.config_flow: continue mqtt = integration.manifest.get("mqtt") diff --git a/script/hassfest/ssdp.py b/script/hassfest/ssdp.py index c71d5432adf..0611f9a2225 100644 --- a/script/hassfest/ssdp.py +++ b/script/hassfest/ssdp.py @@ -31,7 +31,7 @@ def generate_and_validate(integrations: dict[str, Integration]): for domain in sorted(integrations): integration = integrations[domain] - if not integration.manifest: + if not integration.manifest or not integration.config_flow: continue ssdp = integration.manifest.get("ssdp") diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py index 907c6aaceff..4ce4896952e 100644 --- a/script/hassfest/zeroconf.py +++ b/script/hassfest/zeroconf.py @@ -28,7 +28,7 @@ def generate_and_validate(integrations: dict[str, Integration]): for domain in sorted(integrations): integration = integrations[domain] - if not integration.manifest: + if not integration.manifest or not integration.config_flow: continue service_types = integration.manifest.get("zeroconf", []) From 494fd21351e468203529222fce665efe27c4d1e7 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 16 Aug 2021 16:34:22 +0200 Subject: [PATCH 234/355] Refactor mysensors sensor description (#54522) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen --- homeassistant/components/mysensors/sensor.py | 270 +++++++++++-------- 1 file changed, 165 insertions(+), 105 deletions(-) diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 396d0e2519b..68fdf2a21b2 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -1,7 +1,7 @@ """Support for MySensors sensors.""" from __future__ import annotations -from datetime import datetime +from typing import Any from awesomeversion import AwesomeVersion @@ -10,6 +10,7 @@ from homeassistant.components.sensor import ( DOMAIN, STATE_CLASS_MEASUREMENT, SensorEntity, + SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -46,64 +47,150 @@ from homeassistant.util.dt import utc_from_timestamp from .const import MYSENSORS_DISCOVERY, DiscoveryInfo from .helpers import on_unload -SENSORS: dict[str, list[str | None] | dict[str, list[str | None]]] = { - "V_TEMP": [None, None, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT], - "V_HUM": [ - PERCENTAGE, - "mdi:water-percent", - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, - ], - "V_DIMMER": [PERCENTAGE, "mdi:percent", None, None], - "V_PERCENTAGE": [PERCENTAGE, "mdi:percent", None, None], - "V_PRESSURE": [None, "mdi:gauge", None, None], - "V_FORECAST": [None, "mdi:weather-partly-cloudy", None, None], - "V_RAIN": [None, "mdi:weather-rainy", None, None], - "V_RAINRATE": [None, "mdi:weather-rainy", None, None], - "V_WIND": [None, "mdi:weather-windy", None, None], - "V_GUST": [None, "mdi:weather-windy", None, None], - "V_DIRECTION": [DEGREE, "mdi:compass", None, None], - "V_WEIGHT": [MASS_KILOGRAMS, "mdi:weight-kilogram", None, None], - "V_DISTANCE": [LENGTH_METERS, "mdi:ruler", None, None], - "V_IMPEDANCE": ["ohm", None, None, None], - "V_WATT": [POWER_WATT, None, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT], - "V_KWH": [ - ENERGY_KILO_WATT_HOUR, - None, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - ], - "V_LIGHT_LEVEL": [PERCENTAGE, "mdi:white-balance-sunny", None, None], - "V_FLOW": [LENGTH_METERS, "mdi:gauge", None, None], - "V_VOLUME": [VOLUME_CUBIC_METERS, None, None, None], - "V_LEVEL": { - "S_SOUND": [SOUND_PRESSURE_DB, "mdi:volume-high", None, None], - "S_VIBRATION": [FREQUENCY_HERTZ, None, None, None], - "S_LIGHT_LEVEL": [ - LIGHT_LUX, - "mdi:white-balance-sunny", - DEVICE_CLASS_ILLUMINANCE, - STATE_CLASS_MEASUREMENT, - ], - "S_MOISTURE": [PERCENTAGE, "mdi:water-percent", None, None], - }, - "V_VOLTAGE": [ - ELECTRIC_POTENTIAL_VOLT, - "mdi:flash", - DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, - ], - "V_CURRENT": [ - ELECTRIC_CURRENT_AMPERE, - "mdi:flash-auto", - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, - ], - "V_PH": ["pH", None, None, None], - "V_ORP": [ELECTRIC_POTENTIAL_MILLIVOLT, None, None, None], - "V_EC": [CONDUCTIVITY, None, None, None], - "V_VAR": ["var", None, None, None], - "V_VA": [POWER_VOLT_AMPERE, None, None, None], +SENSORS: dict[str, SensorEntityDescription] = { + "V_TEMP": SensorEntityDescription( + key="V_TEMP", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "V_HUM": SensorEntityDescription( + key="V_HUM", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + "V_DIMMER": SensorEntityDescription( + key="V_DIMMER", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:percent", + ), + "V_PERCENTAGE": SensorEntityDescription( + key="V_PERCENTAGE", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:percent", + ), + "V_PRESSURE": SensorEntityDescription( + key="V_PRESSURE", + icon="mdi:gauge", + ), + "V_FORECAST": SensorEntityDescription( + key="V_FORECAST", + icon="mdi:weather-partly-cloudy", + ), + "V_RAIN": SensorEntityDescription( + key="V_RAIN", + icon="mdi:weather-rainy", + ), + "V_RAINRATE": SensorEntityDescription( + key="V_RAINRATE", + icon="mdi:weather-rainy", + ), + "V_WIND": SensorEntityDescription( + key="V_WIND", + icon="mdi:weather-windy", + ), + "V_GUST": SensorEntityDescription( + key="V_GUST", + icon="mdi:weather-windy", + ), + "V_DIRECTION": SensorEntityDescription( + key="V_DIRECTION", + native_unit_of_measurement=DEGREE, + icon="mdi:compass", + ), + "V_WEIGHT": SensorEntityDescription( + key="V_WEIGHT", + native_unit_of_measurement=MASS_KILOGRAMS, + icon="mdi:weight-kilogram", + ), + "V_DISTANCE": SensorEntityDescription( + key="V_DISTANCE", + native_unit_of_measurement=LENGTH_METERS, + icon="mdi:ruler", + ), + "V_IMPEDANCE": SensorEntityDescription( + key="V_IMPEDANCE", + native_unit_of_measurement="ohm", + ), + "V_WATT": SensorEntityDescription( + key="V_WATT", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + "V_KWH": SensorEntityDescription( + key="V_KWH", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=utc_from_timestamp(0), + ), + "V_LIGHT_LEVEL": SensorEntityDescription( + key="V_LIGHT_LEVEL", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:white-balance-sunny", + ), + "V_FLOW": SensorEntityDescription( + key="V_FLOW", + native_unit_of_measurement=LENGTH_METERS, + icon="mdi:gauge", + ), + "V_VOLUME": SensorEntityDescription( + key="V_VOLUME", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + ), + "V_LEVEL_S_SOUND": SensorEntityDescription( + key="V_LEVEL_S_SOUND", + native_unit_of_measurement=SOUND_PRESSURE_DB, + icon="mdi:volume-high", + ), + "V_LEVEL_S_VIBRATION": SensorEntityDescription( + key="V_LEVEL_S_VIBRATION", + native_unit_of_measurement=FREQUENCY_HERTZ, + ), + "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, + ), + "V_LEVEL_S_MOISTURE": SensorEntityDescription( + key="V_LEVEL_S_MOISTURE", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:water-percent", + ), + "V_VOLTAGE": SensorEntityDescription( + key="V_VOLTAGE", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "V_CURRENT": SensorEntityDescription( + key="V_CURRENT", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + "V_PH": SensorEntityDescription( + key="V_PH", + native_unit_of_measurement="pH", + ), + "V_ORP": SensorEntityDescription( + key="V_ORP", + native_unit_of_measurement=ELECTRIC_POTENTIAL_MILLIVOLT, + ), + "V_EC": SensorEntityDescription( + key="V_EC", + native_unit_of_measurement=CONDUCTIVITY, + ), + "V_VAR": SensorEntityDescription( + key="V_VAR", + native_unit_of_measurement="var", + ), + "V_VA": SensorEntityDescription( + key="V_VA", + native_unit_of_measurement=POWER_VOLT_AMPERE, + ), } @@ -138,44 +225,19 @@ async def async_setup_entry( class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity): """Representation of a MySensors Sensor child node.""" - @property - def force_update(self) -> bool: - """Return True if state updates should be forced. + _attr_force_update = True - If True, a state change will be triggered anytime the state property is - updated, not just when the value changes. - """ - return True + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Set up the instance.""" + super().__init__(*args, **kwargs) + if entity_description := self._get_entity_description(): + self.entity_description = entity_description @property def native_value(self) -> str | None: - """Return the state of the device.""" + """Return the state of the sensor.""" return self._values.get(self.value_type) - @property - def device_class(self) -> str | None: - """Return the device class of this entity.""" - return self._get_sensor_type()[2] - - @property - def icon(self) -> str | None: - """Return the icon to use in the frontend, if any.""" - return self._get_sensor_type()[1] - - @property - def last_reset(self) -> datetime | None: - """Return the time when the sensor was last reset, if any.""" - set_req = self.gateway.const.SetReq - - if set_req(self.value_type).name == "V_KWH": - return utc_from_timestamp(0) - return None - - @property - def state_class(self) -> str | None: - """Return the state class of this entity.""" - return self._get_sensor_type()[3] - @property def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity.""" @@ -192,21 +254,19 @@ class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity): return TEMP_CELSIUS return TEMP_FAHRENHEIT - unit = self._get_sensor_type()[0] - return unit + if hasattr(self, "entity_description"): + return self.entity_description.native_unit_of_measurement + return None - def _get_sensor_type(self) -> list[str | None]: - """Return list with unit and icon of sensor type.""" - pres = self.gateway.const.Presentation + def _get_entity_description(self) -> SensorEntityDescription | None: + """Return the sensor entity description.""" set_req = self.gateway.const.SetReq + entity_description = SENSORS.get(set_req(self.value_type).name) - _sensor_type = SENSORS.get( - set_req(self.value_type).name, [None, None, None, None] - ) - if isinstance(_sensor_type, dict): - sensor_type = _sensor_type.get( - pres(self.child_type).name, [None, None, None, None] + if not entity_description: + pres = self.gateway.const.Presentation + entity_description = SENSORS.get( + f"{set_req(self.value_type).name}_{pres(self.child_type).name}" ) - else: - sensor_type = _sensor_type - return sensor_type + + return entity_description From 1b256efb23631ff5ec23749b442281279046f7e8 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 16 Aug 2021 09:26:17 -0600 Subject: [PATCH 235/355] Bump simplisafe-python to 11.0.4 (#54701) --- 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 8c23e575cc3..6bf029ead6e 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==11.0.3"], + "requirements": ["simplisafe-python==11.0.4"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 9203b216eb7..13018dcc9b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2114,7 +2114,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==11.0.3 +simplisafe-python==11.0.4 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2e8f737214..09b17f6415e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1165,7 +1165,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==11.0.3 +simplisafe-python==11.0.4 # homeassistant.components.slack slackclient==2.5.0 From 2b1299b540dd9c162f3148127c69962fa2aa2a8d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 Aug 2021 18:20:44 +0200 Subject: [PATCH 236/355] Update Toon to use new state classes (#54705) --- homeassistant/components/toon/const.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/toon/const.py b/homeassistant/components/toon/const.py index 1c9192c4544..1c58ec2cde7 100644 --- a/homeassistant/components/toon/const.py +++ b/homeassistant/components/toon/const.py @@ -6,12 +6,12 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_PROBLEM, ) from homeassistant.components.sensor import ( - ATTR_LAST_RESET, ATTR_STATE_CLASS, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -25,7 +25,6 @@ from homeassistant.const import ( TEMP_CELSIUS, VOLUME_CUBIC_METERS, ) -from homeassistant.util import dt as dt_util DOMAIN = "toon" @@ -152,9 +151,8 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "meter", ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, ATTR_ICON: "mdi:gas-cylinder", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, ATTR_DEVICE_CLASS: DEVICE_CLASS_GAS, - ATTR_LAST_RESET: dt_util.utc_from_timestamp(0), ATTR_DEFAULT_ENABLED: False, }, "gas_value": { @@ -200,8 +198,7 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "meter_high", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: dt_util.utc_from_timestamp(0), + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, ATTR_DEFAULT_ENABLED: False, }, "power_meter_reading_low": { @@ -210,8 +207,7 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "meter_low", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: dt_util.utc_from_timestamp(0), + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, ATTR_DEFAULT_ENABLED: False, }, "power_value": { @@ -228,8 +224,7 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "meter_produced_high", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: dt_util.utc_from_timestamp(0), + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, ATTR_DEFAULT_ENABLED: False, }, "solar_meter_reading_low_produced": { @@ -238,8 +233,7 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "meter_produced_low", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: dt_util.utc_from_timestamp(0), + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, ATTR_DEFAULT_ENABLED: False, }, "solar_value": { @@ -344,8 +338,7 @@ SENSOR_ENTITIES = { ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, ATTR_ICON: "mdi:water", ATTR_DEFAULT_ENABLED: False, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: dt_util.utc_from_timestamp(0), + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, "water_value": { ATTR_NAME: "Current Water Usage", From 61ab2b0c60ceb58cfd7dabec99c125830d2185d3 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 16 Aug 2021 12:30:52 -0400 Subject: [PATCH 237/355] Use zwave_js.number platform for Basic CC values (#54512) * Use zwave_js.number platform for some Basic CC values * Remove Basic CC sensor discovery schema * update comment * update comment --- homeassistant/components/zwave_js/discovery.py | 15 ++++++++++++--- homeassistant/components/zwave_js/sensor.py | 16 ---------------- tests/components/zwave_js/common.py | 2 +- tests/components/zwave_js/test_number.py | 14 ++++++++++++++ tests/components/zwave_js/test_sensor.py | 11 ----------- 5 files changed, 27 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 588b4c76472..77590e780a5 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -542,10 +542,10 @@ DISCOVERY_SCHEMAS = [ allow_multi=True, entity_registry_enabled_default=False, ), - # sensor for basic CC + # number for Basic CC ZWaveDiscoverySchema( - platform="sensor", - hint="numeric_sensor", + platform="number", + hint="Basic", primary_value=ZWaveValueDiscoverySchema( command_class={ CommandClass.BASIC, @@ -553,6 +553,15 @@ DISCOVERY_SCHEMAS = [ type={"number"}, property={"currentValue"}, ), + required_values=[ + ZWaveValueDiscoverySchema( + command_class={ + CommandClass.BASIC, + }, + type={"number"}, + property={"targetValue"}, + ) + ], entity_registry_enabled_default=False, ), # binary switches diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index aa163fa8bd9..deacf3d874a 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -198,22 +198,6 @@ class ZWaveStringSensor(ZwaveSensorBase): class ZWaveNumericSensor(ZwaveSensorBase): """Representation of a Z-Wave Numeric sensor.""" - def __init__( - self, - config_entry: ConfigEntry, - client: ZwaveClient, - info: ZwaveDiscoveryInfo, - ) -> None: - """Initialize a ZWaveNumericSensor entity.""" - super().__init__(config_entry, client, info) - - # Entity class attributes - if self.info.primary_value.command_class == CommandClass.BASIC: - self._attr_name = self.generate_name( - include_value_name=True, - alternate_value_name=self.info.primary_value.command_class_name, - ) - @property def native_value(self) -> float: """Return state of the sensor.""" diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index 44943fed9fb..0c6b19698a9 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -16,7 +16,7 @@ NOTIFICATION_MOTION_BINARY_SENSOR = ( ) NOTIFICATION_MOTION_SENSOR = "sensor.multisensor_6_home_security_motion_sensor_status" INDICATOR_SENSOR = "sensor.z_wave_thermostat_indicator_value" -BASIC_SENSOR = "sensor.livingroomlight_basic" +BASIC_NUMBER_ENTITY = "number.livingroomlight_basic" PROPERTY_DOOR_STATUS_BINARY_SENSOR = ( "binary_sensor.august_smart_lock_pro_3rd_gen_the_current_status_of_the_door" ) diff --git a/tests/components/zwave_js/test_number.py b/tests/components/zwave_js/test_number.py index b7d83068bea..6439d034587 100644 --- a/tests/components/zwave_js/test_number.py +++ b/tests/components/zwave_js/test_number.py @@ -1,6 +1,10 @@ """Test the Z-Wave JS number platform.""" from zwave_js_server.event import Event +from homeassistant.helpers import entity_registry as er + +from .common import BASIC_NUMBER_ENTITY + NUMBER_ENTITY = "number.thermostat_hvac_valve_control" @@ -67,3 +71,13 @@ async def test_number(hass, client, aeotec_radiator_thermostat, integration): state = hass.states.get(NUMBER_ENTITY) assert state.state == "99.0" + + +async def test_disabled_basic_number(hass, ge_in_wall_dimmer_switch, integration): + """Test number is created from Basic CC and is disabled.""" + ent_reg = er.async_get(hass) + entity_entry = ent_reg.async_get(BASIC_NUMBER_ENTITY) + + assert entity_entry + assert entity_entry.disabled + assert entity_entry.disabled_by == er.DISABLED_INTEGRATION diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index 04583559421..268d8ee1380 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -28,7 +28,6 @@ from homeassistant.helpers import entity_registry as er from .common import ( AIR_TEMPERATURE_SENSOR, - BASIC_SENSOR, CURRENT_SENSOR, DATETIME_LAST_RESET, DATETIME_ZERO, @@ -131,16 +130,6 @@ async def test_disabled_indcator_sensor( assert entity_entry.disabled_by == er.DISABLED_INTEGRATION -async def test_disabled_basic_sensor(hass, ge_in_wall_dimmer_switch, integration): - """Test sensor is created from Basic CC and is disabled.""" - ent_reg = er.async_get(hass) - entity_entry = ent_reg.async_get(BASIC_SENSOR) - - assert entity_entry - assert entity_entry.disabled - assert entity_entry.disabled_by == er.DISABLED_INTEGRATION - - async def test_config_parameter_sensor(hass, lock_id_lock_as_id150, integration): """Test config parameter sensor is created.""" ent_reg = er.async_get(hass) From 35389a6d28d417bf0f41c08cb945b592e7f21fab Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 16 Aug 2021 18:35:50 +0200 Subject: [PATCH 238/355] Remove last_reset attribute from dsmr sensors (#54699) --- homeassistant/components/dsmr/const.py | 36 ++++++++++---------------- tests/components/dsmr/test_sensor.py | 35 ++++++++++++++++--------- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index b5fb74bbbe6..0043113772e 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -5,7 +5,10 @@ import logging from dsmr_parser import obis_references -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, +) from homeassistant.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, @@ -13,7 +16,6 @@ from homeassistant.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, ) -from homeassistant.util import dt from .models import DSMRSensorEntityDescription @@ -67,32 +69,28 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( name="Energy Consumption (tarif 1)", device_class=DEVICE_CLASS_ENERGY, force_update=True, - last_reset=dt.utc_from_timestamp(0), - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.ELECTRICITY_USED_TARIFF_2, name="Energy Consumption (tarif 2)", force_update=True, device_class=DEVICE_CLASS_ENERGY, - last_reset=dt.utc_from_timestamp(0), - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.ELECTRICITY_DELIVERED_TARIFF_1, name="Energy Production (tarif 1)", force_update=True, device_class=DEVICE_CLASS_ENERGY, - last_reset=dt.utc_from_timestamp(0), - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.ELECTRICITY_DELIVERED_TARIFF_2, name="Energy Production (tarif 2)", force_update=True, device_class=DEVICE_CLASS_ENERGY, - last_reset=dt.utc_from_timestamp(0), - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, @@ -229,8 +227,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( dsmr_versions={"5L"}, force_update=True, device_class=DEVICE_CLASS_ENERGY, - last_reset=dt.utc_from_timestamp(0), - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, @@ -238,8 +235,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( dsmr_versions={"5L"}, force_update=True, device_class=DEVICE_CLASS_ENERGY, - last_reset=dt.utc_from_timestamp(0), - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.ELECTRICITY_IMPORTED_TOTAL, @@ -247,8 +243,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( dsmr_versions={"2.2", "4", "5", "5B"}, force_update=True, device_class=DEVICE_CLASS_ENERGY, - last_reset=dt.utc_from_timestamp(0), - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.HOURLY_GAS_METER_READING, @@ -258,8 +253,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( force_update=True, icon="mdi:fire", device_class=DEVICE_CLASS_GAS, - last_reset=dt.utc_from_timestamp(0), - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.BELGIUM_HOURLY_GAS_METER_READING, @@ -269,8 +263,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( force_update=True, icon="mdi:fire", device_class=DEVICE_CLASS_GAS, - last_reset=dt.utc_from_timestamp(0), - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRSensorEntityDescription( key=obis_references.GAS_METER_READING, @@ -280,7 +273,6 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( force_update=True, icon="mdi:fire", device_class=DEVICE_CLASS_GAS, - last_reset=dt.utc_from_timestamp(0), - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, ), ) diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index c7e0addd800..0f1c55f47b6 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -18,6 +18,7 @@ from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -167,8 +168,10 @@ async def test_default_setup(hass, dsmr_connection_fixture): assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" - assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None - assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert gas_consumption.attributes.get(ATTR_LAST_RESET) is None + assert ( + gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + ) assert ( gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS ) @@ -267,8 +270,10 @@ async def test_v4_meter(hass, dsmr_connection_fixture): assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" - assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None - assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert gas_consumption.attributes.get(ATTR_LAST_RESET) is None + assert ( + gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + ) assert ( gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS ) @@ -337,8 +342,10 @@ async def test_v5_meter(hass, dsmr_connection_fixture): assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" - assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None - assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert gas_consumption.attributes.get(ATTR_LAST_RESET) is None + assert ( + gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + ) assert ( gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS ) @@ -403,8 +410,8 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture): assert power_tariff.state == "123.456" assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY assert power_tariff.attributes.get(ATTR_ICON) is None - assert power_tariff.attributes.get(ATTR_LAST_RESET) is not None - assert power_tariff.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert power_tariff.attributes.get(ATTR_LAST_RESET) is None + assert power_tariff.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING assert ( power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR ) @@ -418,8 +425,10 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture): assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" - assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None - assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert gas_consumption.attributes.get(ATTR_LAST_RESET) is None + assert ( + gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + ) assert ( gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS ) @@ -488,8 +497,10 @@ async def test_belgian_meter(hass, dsmr_connection_fixture): assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is DEVICE_CLASS_GAS assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" - assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None - assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert gas_consumption.attributes.get(ATTR_LAST_RESET) is None + assert ( + gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + ) assert ( gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS ) From 441552e04cb4e8d08287e6cd7a4d4b2b1c9561ed Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Mon, 16 Aug 2021 13:02:01 -0400 Subject: [PATCH 239/355] Fix TypeError when climate component sets fan modes to None (#54709) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen --- homeassistant/components/google_assistant/trait.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 36222902296..06d10c5372b 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1290,7 +1290,7 @@ class FanSpeedTrait(_Trait): ) elif domain == climate.DOMAIN: - modes = self.state.attributes.get(climate.ATTR_FAN_MODES, []) + modes = self.state.attributes.get(climate.ATTR_FAN_MODES) or [] for mode in modes: speed = { "speed_name": mode, From a41ee9e870f5134a9199fbadd853bfa0d3612128 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 16 Aug 2021 13:36:20 -0400 Subject: [PATCH 240/355] Create zwave-js select platform and discover additional siren values (#53018) * Create zwave-js select platform and add siren values to number and select platforms * use constants while we wait for lib release * comments * rename stuff in tests to prepare for protection CC PR * Switch to 0-1 range for number entity * Update homeassistant/components/zwave_js/number.py Co-authored-by: kpine * Change step * Switch to ToneID * Better error handling * Add test for coerage Co-authored-by: kpine --- .../components/zwave_js/discovery.py | 24 +++++ homeassistant/components/zwave_js/number.py | 40 ++++++- homeassistant/components/zwave_js/select.py | 91 ++++++++++++++++ tests/components/zwave_js/test_number.py | 94 ++++++++++++++++ tests/components/zwave_js/test_select.py | 101 ++++++++++++++++++ 5 files changed, 349 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/zwave_js/select.py create mode 100644 tests/components/zwave_js/test_select.py diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 77590e780a5..58dae39781e 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -642,6 +642,30 @@ DISCOVERY_SCHEMAS = [ platform="siren", primary_value=SIREN_TONE_SCHEMA, ), + # select + # siren default tone + ZWaveDiscoverySchema( + platform="select", + hint="Default tone", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SOUND_SWITCH}, + property={"defaultToneId"}, + type={"number"}, + ), + required_values=[SIREN_TONE_SCHEMA], + ), + # number + # siren default volume + ZWaveDiscoverySchema( + platform="number", + hint="volume", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SOUND_SWITCH}, + property={"defaultVolume"}, + type={"number"}, + ), + required_values=[SIREN_TONE_SCHEMA], + ), ] diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py index e53e5942999..675a396fb7b 100644 --- a/homeassistant/components/zwave_js/number.py +++ b/homeassistant/components/zwave_js/number.py @@ -26,7 +26,10 @@ async def async_setup_entry( def async_add_number(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave number entity.""" entities: list[ZWaveBaseEntity] = [] - entities.append(ZwaveNumberEntity(config_entry, client, info)) + if info.platform_hint == "volume": + entities.append(ZwaveVolumeNumberEntity(config_entry, client, info)) + else: + entities.append(ZwaveNumberEntity(config_entry, client, info)) async_add_entities(entities) config_entry.async_on_unload( @@ -87,3 +90,38 @@ class ZwaveNumberEntity(ZWaveBaseEntity, NumberEntity): async def async_set_value(self, value: float) -> None: """Set new value.""" await self.info.node.async_set_value(self._target_value, value) + + +class ZwaveVolumeNumberEntity(ZWaveBaseEntity, NumberEntity): + """Representation of a volume number entity.""" + + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize a ZwaveVolumeNumberEntity entity.""" + super().__init__(config_entry, client, info) + self.correction_factor = int( + self.info.primary_value.metadata.max - self.info.primary_value.metadata.min + ) + # Fallback in case we can't properly calculate correction factor + if self.correction_factor == 0: + self.correction_factor = 1 + + # Entity class attributes + self._attr_min_value = 0 + self._attr_max_value = 1 + self._attr_step = 0.01 + self._attr_name = self.generate_name(include_value_name=True) + + @property + def value(self) -> float | None: + """Return the entity value.""" + if self.info.primary_value.value is None: + return None + return float(self.info.primary_value.value) / self.correction_factor + + async def async_set_value(self, value: float) -> None: + """Set new value.""" + await self.info.node.async_set_value( + self.info.primary_value, round(value * self.correction_factor) + ) diff --git a/homeassistant/components/zwave_js/select.py b/homeassistant/components/zwave_js/select.py new file mode 100644 index 00000000000..2bd711bfde3 --- /dev/null +++ b/homeassistant/components/zwave_js/select.py @@ -0,0 +1,91 @@ +"""Support for Z-Wave controls using the select platform.""" +from __future__ import annotations + +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.const import CommandClass, ToneID + +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN, SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DATA_CLIENT, DOMAIN +from .discovery import ZwaveDiscoveryInfo +from .entity import ZWaveBaseEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Z-Wave Select entity from Config Entry.""" + client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + + @callback + def async_add_select(info: ZwaveDiscoveryInfo) -> None: + """Add Z-Wave select entity.""" + entities: list[ZWaveBaseEntity] = [] + if info.platform_hint == "Default tone": + entities.append(ZwaveDefaultToneSelectEntity(config_entry, client, info)) + async_add_entities(entities) + + config_entry.async_on_unload( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{config_entry.entry_id}_add_{SELECT_DOMAIN}", + async_add_select, + ) + ) + + +class ZwaveDefaultToneSelectEntity(ZWaveBaseEntity, SelectEntity): + """Representation of a Z-Wave default tone select entity.""" + + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize a ZwaveDefaultToneSelectEntity entity.""" + super().__init__(config_entry, client, info) + self._tones_value = self.get_zwave_value( + "toneId", command_class=CommandClass.SOUND_SWITCH + ) + + # Entity class attributes + self._attr_name = self.generate_name( + include_value_name=True, alternate_value_name=info.platform_hint + ) + + @property + def options(self) -> list[str]: + """Return a set of selectable options.""" + # We know we can assert because this value is part of the discovery schema + assert self._tones_value + return [ + val + for key, val in self._tones_value.metadata.states.items() + if int(key) not in (ToneID.DEFAULT, ToneID.OFF) + ] + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + # We know we can assert because this value is part of the discovery schema + assert self._tones_value + return str( + self._tones_value.metadata.states.get( + str(self.info.primary_value.value), self.info.primary_value.value + ) + ) + + async def async_select_option(self, option: str | int) -> None: + """Change the selected option.""" + # We know we can assert because this value is part of the discovery schema + assert self._tones_value + key = next( + key + for key, val in self._tones_value.metadata.states.items() + if val == option + ) + await self.info.node.async_set_value(self.info.primary_value, int(key)) diff --git a/tests/components/zwave_js/test_number.py b/tests/components/zwave_js/test_number.py index 6439d034587..6d9458d096c 100644 --- a/tests/components/zwave_js/test_number.py +++ b/tests/components/zwave_js/test_number.py @@ -1,11 +1,13 @@ """Test the Z-Wave JS number platform.""" from zwave_js_server.event import Event +from homeassistant.const import STATE_UNKNOWN from homeassistant.helpers import entity_registry as er from .common import BASIC_NUMBER_ENTITY NUMBER_ENTITY = "number.thermostat_hvac_valve_control" +VOLUME_NUMBER_ENTITY = "number.indoor_siren_6_default_volume_2" async def test_number(hass, client, aeotec_radiator_thermostat, integration): @@ -73,6 +75,98 @@ async def test_number(hass, client, aeotec_radiator_thermostat, integration): assert state.state == "99.0" +async def test_volume_number(hass, client, aeotec_zw164_siren, integration): + """Test the volume number entity.""" + node = aeotec_zw164_siren + state = hass.states.get(VOLUME_NUMBER_ENTITY) + + assert state + assert state.state == "1.0" + assert state.attributes["step"] == 0.01 + assert state.attributes["max"] == 1.0 + assert state.attributes["min"] == 0 + + # Test turn on setting value + await hass.services.async_call( + "number", + "set_value", + {"entity_id": VOLUME_NUMBER_ENTITY, "value": 0.3}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == node.node_id + assert args["valueId"] == { + "endpoint": 2, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "defaultVolume", + "propertyName": "defaultVolume", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "label": "Default volume", + "min": 0, + "max": 100, + "unit": "%", + }, + "value": 100, + } + assert args["value"] == 30 + + client.async_send_command.reset_mock() + + # Test value update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 4, + "args": { + "commandClassName": "Sound Switch", + "commandClass": 121, + "endpoint": 2, + "property": "defaultVolume", + "newValue": 30, + "prevValue": 100, + "propertyName": "defaultVolume", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(VOLUME_NUMBER_ENTITY) + assert state.state == "0.3" + + # Test null value + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 4, + "args": { + "commandClassName": "Sound Switch", + "commandClass": 121, + "endpoint": 2, + "property": "defaultVolume", + "newValue": None, + "prevValue": 30, + "propertyName": "defaultVolume", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(VOLUME_NUMBER_ENTITY) + assert state.state == STATE_UNKNOWN + + async def test_disabled_basic_number(hass, ge_in_wall_dimmer_switch, integration): """Test number is created from Basic CC and is disabled.""" ent_reg = er.async_get(hass) diff --git a/tests/components/zwave_js/test_select.py b/tests/components/zwave_js/test_select.py new file mode 100644 index 00000000000..b94bac812b6 --- /dev/null +++ b/tests/components/zwave_js/test_select.py @@ -0,0 +1,101 @@ +"""Test the Z-Wave JS number platform.""" +from zwave_js_server.event import Event + +DEFAULT_TONE_SELECT_ENTITY = "select.indoor_siren_6_default_tone_2" + + +async def test_default_tone_select(hass, client, aeotec_zw164_siren, integration): + """Test the default tone select entity.""" + node = aeotec_zw164_siren + state = hass.states.get(DEFAULT_TONE_SELECT_ENTITY) + + assert state + assert state.state == "17ALAR~1 (35 sec)" + attr = state.attributes + assert attr["options"] == [ + "01DING~1 (5 sec)", + "02DING~1 (9 sec)", + "03TRAD~1 (11 sec)", + "04ELEC~1 (2 sec)", + "05WEST~1 (13 sec)", + "06CHIM~1 (7 sec)", + "07CUCK~1 (31 sec)", + "08TRAD~1 (6 sec)", + "09SMOK~1 (11 sec)", + "10SMOK~1 (6 sec)", + "11FIRE~1 (35 sec)", + "12COSE~1 (5 sec)", + "13KLAX~1 (38 sec)", + "14DEEP~1 (41 sec)", + "15WARN~1 (37 sec)", + "16TORN~1 (46 sec)", + "17ALAR~1 (35 sec)", + "18DEEP~1 (62 sec)", + "19ALAR~1 (15 sec)", + "20ALAR~1 (7 sec)", + "21DIGI~1 (8 sec)", + "22ALER~1 (64 sec)", + "23SHIP~1 (4 sec)", + "25CHRI~1 (4 sec)", + "26GONG~1 (12 sec)", + "27SING~1 (1 sec)", + "28TONA~1 (5 sec)", + "29UPWA~1 (2 sec)", + "30DOOR~1 (27 sec)", + ] + + # Test select option with string value + await hass.services.async_call( + "select", + "select_option", + {"entity_id": DEFAULT_TONE_SELECT_ENTITY, "option": "30DOOR~1 (27 sec)"}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == node.node_id + assert args["valueId"] == { + "endpoint": 2, + "commandClass": 121, + "commandClassName": "Sound Switch", + "property": "defaultToneId", + "propertyName": "defaultToneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "label": "Default tone ID", + "min": 0, + "max": 254, + }, + "value": 17, + } + assert args["value"] == 30 + + client.async_send_command.reset_mock() + + # Test value update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Sound Switch", + "commandClass": 121, + "endpoint": 2, + "property": "defaultToneId", + "newValue": 30, + "prevValue": 17, + "propertyName": "defaultToneId", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(DEFAULT_TONE_SELECT_ENTITY) + assert state.state == "30DOOR~1 (27 sec)" From c68253b5801715ae33bb91c58e04df7b2b5a44c6 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Mon, 16 Aug 2021 19:37:49 +0200 Subject: [PATCH 241/355] Fix AsusWRT scanner entity DeviceInfo (#54648) --- .../components/asuswrt/device_tracker.py | 11 +++--- homeassistant/components/asuswrt/sensor.py | 38 ++++++++----------- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py index 0b5d81e3de9..3e954eb25b9 100644 --- a/homeassistant/components/asuswrt/device_tracker.py +++ b/homeassistant/components/asuswrt/device_tracker.py @@ -60,6 +60,12 @@ class AsusWrtDevice(ScannerEntity): self._device = device self._attr_unique_id = device.mac self._attr_name = device.name or DEFAULT_DEVICE_NAME + self._attr_device_info = { + "connections": {(CONNECTION_NETWORK_MAC, device.mac)}, + "default_model": "ASUSWRT Tracked device", + } + if device.name: + self._attr_device_info["default_name"] = device.name @property def is_connected(self): @@ -90,11 +96,6 @@ class AsusWrtDevice(ScannerEntity): def async_on_demand_update(self): """Update state.""" self._device = self._router.devices[self._device.mac] - self._attr_device_info = { - "connections": {(CONNECTION_NETWORK_MAC, self._device.mac)}, - } - if self._device.name: - self._attr_device_info["default_name"] = self._device.name self._attr_extra_state_attributes = {} if self._device.last_activity: self._attr_extra_state_attributes[ diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 3367cc37ee4..a9a005b9837 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -4,22 +4,20 @@ from __future__ import annotations from dataclasses import dataclass import logging from numbers import Number -from typing import Any from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -from homeassistant.util import dt as dt_util from .const import ( DATA_ASUSWRT, @@ -48,12 +46,14 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( key=SENSORS_CONNECTED_DEVICE[0], name="Devices Connected", icon="mdi:router-network", + state_class=STATE_CLASS_MEASUREMENT, native_unit_of_measurement=UNIT_DEVICES, ), AsusWrtSensorEntityDescription( key=SENSORS_RATES[0], name="Download Speed", icon="mdi:download-network", + state_class=STATE_CLASS_MEASUREMENT, native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, entity_registry_enabled_default=False, factor=125000, @@ -62,6 +62,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( key=SENSORS_RATES[1], name="Upload Speed", icon="mdi:upload-network", + state_class=STATE_CLASS_MEASUREMENT, native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, entity_registry_enabled_default=False, factor=125000, @@ -70,6 +71,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( key=SENSORS_BYTES[0], name="Download", icon="mdi:download", + state_class=STATE_CLASS_TOTAL_INCREASING, native_unit_of_measurement=DATA_GIGABYTES, entity_registry_enabled_default=False, factor=1000000000, @@ -78,6 +80,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( key=SENSORS_BYTES[1], name="Upload", icon="mdi:upload", + state_class=STATE_CLASS_TOTAL_INCREASING, native_unit_of_measurement=DATA_GIGABYTES, entity_registry_enabled_default=False, factor=1000000000, @@ -86,6 +89,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( key=SENSORS_LOAD_AVG[0], name="Load Avg (1m)", icon="mdi:cpu-32-bit", + state_class=STATE_CLASS_MEASUREMENT, entity_registry_enabled_default=False, factor=1, precision=1, @@ -94,6 +98,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( key=SENSORS_LOAD_AVG[1], name="Load Avg (5m)", icon="mdi:cpu-32-bit", + state_class=STATE_CLASS_MEASUREMENT, entity_registry_enabled_default=False, factor=1, precision=1, @@ -102,6 +107,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( key=SENSORS_LOAD_AVG[2], name="Load Avg (15m)", icon="mdi:cpu-32-bit", + state_class=STATE_CLASS_MEASUREMENT, entity_registry_enabled_default=False, factor=1, precision=1, @@ -143,33 +149,19 @@ class AsusWrtSensor(CoordinatorEntity, SensorEntity): ) -> None: """Initialize a AsusWrt sensor.""" super().__init__(coordinator) - self._router = router self.entity_description = description self._attr_name = f"{DEFAULT_PREFIX} {description.name}" self._attr_unique_id = f"{DOMAIN} {self.name}" - self._attr_state_class = STATE_CLASS_MEASUREMENT - - if description.native_unit_of_measurement == DATA_GIGABYTES: - self._attr_last_reset = dt_util.utc_from_timestamp(0) + self._attr_device_info = router.device_info + self._attr_extra_state_attributes = {"hostname": router.host} @property - def native_value(self) -> str: + def native_value(self) -> str | None: """Return current state.""" descr = self.entity_description state = self.coordinator.data.get(descr.key) - if state is None: - return None - if descr.factor and isinstance(state, Number): - return round(state / descr.factor, descr.precision) + if state is not None: + if descr.factor and isinstance(state, Number): + return round(state / descr.factor, descr.precision) return state - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the attributes.""" - return {"hostname": self._router.host} - - @property - def device_info(self) -> DeviceInfo: - """Return the device information.""" - return self._router.device_info From f40c672cd25ebbbaad5abcdd9e48d8f822210741 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Mon, 16 Aug 2021 13:52:53 -0600 Subject: [PATCH 242/355] Add light platform to MyQ (#54611) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + CODEOWNERS | 2 +- homeassistant/components/myq/const.py | 14 ++- homeassistant/components/myq/light.py | 115 +++++++++++++++++++++ homeassistant/components/myq/manifest.json | 2 +- tests/components/myq/test_light.py | 36 +++++++ tests/fixtures/myq/devices.json | 34 +++++- 7 files changed, 198 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/myq/light.py create mode 100644 tests/components/myq/test_light.py diff --git a/.coveragerc b/.coveragerc index 3795f7e49b8..2cc3bf2d019 100644 --- a/.coveragerc +++ b/.coveragerc @@ -672,6 +672,7 @@ omit = homeassistant/components/mystrom/switch.py homeassistant/components/myq/__init__.py homeassistant/components/myq/cover.py + homeassistant/components/myq/light.py homeassistant/components/nad/media_player.py homeassistant/components/nanoleaf/light.py homeassistant/components/neato/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 4b7cb8520b0..642de7a04d8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -321,7 +321,7 @@ homeassistant/components/msteams/* @peroyvind homeassistant/components/mullvad/* @meichthys homeassistant/components/mutesync/* @currentoor homeassistant/components/my/* @home-assistant/core -homeassistant/components/myq/* @bdraco +homeassistant/components/myq/* @bdraco @ehendrix23 homeassistant/components/mysensors/* @MartinHjelmare @functionpointer homeassistant/components/mystrom/* @fabaff homeassistant/components/nam/* @bieniu diff --git a/homeassistant/components/myq/const.py b/homeassistant/components/myq/const.py index 6189b1601ea..9f3a434ae37 100644 --- a/homeassistant/components/myq/const.py +++ b/homeassistant/components/myq/const.py @@ -5,18 +5,28 @@ from pymyq.garagedoor import ( STATE_OPEN as MYQ_COVER_STATE_OPEN, STATE_OPENING as MYQ_COVER_STATE_OPENING, ) +from pymyq.lamp import STATE_OFF as MYQ_LIGHT_STATE_OFF, STATE_ON as MYQ_LIGHT_STATE_ON -from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING +from homeassistant.const import ( + STATE_CLOSED, + STATE_CLOSING, + STATE_OFF, + STATE_ON, + STATE_OPEN, + STATE_OPENING, +) DOMAIN = "myq" -PLATFORMS = ["cover", "binary_sensor"] +PLATFORMS = ["cover", "binary_sensor", "light"] MYQ_TO_HASS = { MYQ_COVER_STATE_CLOSED: STATE_CLOSED, MYQ_COVER_STATE_CLOSING: STATE_CLOSING, MYQ_COVER_STATE_OPEN: STATE_OPEN, MYQ_COVER_STATE_OPENING: STATE_OPENING, + MYQ_LIGHT_STATE_ON: STATE_ON, + MYQ_LIGHT_STATE_OFF: STATE_OFF, } MYQ_GATEWAY = "myq_gateway" diff --git a/homeassistant/components/myq/light.py b/homeassistant/components/myq/light.py new file mode 100644 index 00000000000..f26d28fe3a3 --- /dev/null +++ b/homeassistant/components/myq/light.py @@ -0,0 +1,115 @@ +"""Support for MyQ-Enabled lights.""" +import logging + +from pymyq.const import ( + DEVICE_STATE as MYQ_DEVICE_STATE, + DEVICE_STATE_ONLINE as MYQ_DEVICE_STATE_ONLINE, + KNOWN_MODELS, + MANUFACTURER, +) +from pymyq.errors import MyQError + +from homeassistant.components.light import LightEntity +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, MYQ_TO_HASS + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up myq lights.""" + data = hass.data[DOMAIN][config_entry.entry_id] + myq = data[MYQ_GATEWAY] + coordinator = data[MYQ_COORDINATOR] + + async_add_entities( + [MyQLight(coordinator, device) for device in myq.lamps.values()], True + ) + + +class MyQLight(CoordinatorEntity, LightEntity): + """Representation of a MyQ light.""" + + _attr_supported_features = 0 + + def __init__(self, coordinator, device): + """Initialize with API object, device id.""" + super().__init__(coordinator) + self._device = device + self._attr_unique_id = device.device_id + self._attr_name = device.name + + @property + def available(self): + """Return if the device is online.""" + if not super().available: + return False + + # Not all devices report online so assume True if its missing + return self._device.device_json[MYQ_DEVICE_STATE].get( + MYQ_DEVICE_STATE_ONLINE, True + ) + + @property + def is_on(self): + """Return true if the light is on, else False.""" + return MYQ_TO_HASS.get(self._device.state) == STATE_ON + + @property + def is_off(self): + """Return true if the light is off, else False.""" + return MYQ_TO_HASS.get(self._device.state) == STATE_OFF + + async def async_turn_on(self, **kwargs): + """Issue on command to light.""" + if self.is_on: + return + + try: + await self._device.turnon(wait_for_state=True) + except MyQError as err: + raise HomeAssistantError( + f"Turning light {self._device.name} on failed with error: {err}" + ) from err + + # Write new state to HASS + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs): + """Issue off command to light.""" + if self.is_off: + return + + try: + await self._device.turnoff(wait_for_state=True) + except MyQError as err: + raise HomeAssistantError( + f"Turning light {self._device.name} off failed with error: {err}" + ) from err + + # Write opening state to HASS + self.async_write_ha_state() + + @property + def device_info(self): + """Return the device_info of the device.""" + device_info = { + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self._device.name, + "manufacturer": MANUFACTURER, + "sw_version": self._device.firmware_version, + } + if model := KNOWN_MODELS.get(self._device.device_id[2:4]): + device_info["model"] = model + if self._device.parent_device_id: + device_info["via_device"] = (DOMAIN, self._device.parent_device_id) + return device_info + + async def async_added_to_hass(self): + """Subscribe to updates.""" + self.async_on_remove( + self.coordinator.async_add_listener(self.async_write_ha_state) + ) diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index a4de12290f1..33cbea71bcd 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -3,7 +3,7 @@ "name": "MyQ", "documentation": "https://www.home-assistant.io/integrations/myq", "requirements": ["pymyq==3.1.2"], - "codeowners": ["@bdraco"], + "codeowners": ["@bdraco","@ehendrix23"], "config_flow": true, "homekit": { "models": ["819LMB", "MYQ"] diff --git a/tests/components/myq/test_light.py b/tests/components/myq/test_light.py new file mode 100644 index 00000000000..c7b3dbc8427 --- /dev/null +++ b/tests/components/myq/test_light.py @@ -0,0 +1,36 @@ +"""The scene tests for the myq platform.""" + +from homeassistant.const import STATE_OFF, STATE_ON + +from .util import async_init_integration + + +async def test_create_lights(hass): + """Test creation of lights.""" + + await async_init_integration(hass) + + state = hass.states.get("light.garage_door_light_off") + assert state.state == STATE_OFF + expected_attributes = { + "friendly_name": "Garage Door Light Off", + "supported_features": 0, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) + + state = hass.states.get("light.garage_door_light_on") + assert state.state == STATE_ON + expected_attributes = { + "friendly_name": "Garage Door Light On", + "supported_features": 0, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) diff --git a/tests/fixtures/myq/devices.json b/tests/fixtures/myq/devices.json index f7c65c6bb20..1e731ffe204 100644 --- a/tests/fixtures/myq/devices.json +++ b/tests/fixtures/myq/devices.json @@ -1,5 +1,5 @@ { - "count" : 4, + "count" : 6, "href" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices", "items" : [ { @@ -128,6 +128,36 @@ "href" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/small_garage_serial", "device_type" : "wifigaragedooropener", "created_date" : "2020-02-10T23:11:47.487" - } + }, + { + "serial_number" : "garage_light_off", + "state" : { + "last_status" : "2020-03-30T02:48:45.7501595Z", + "online" : true, + "lamp_state" : "off", + "last_update" : "2020-03-26T15:45:31.4713796Z" + }, + "parent_device_id" : "gateway_serial", + "device_platform" : "myq", + "name" : "Garage Door Light Off", + "device_family" : "lamp", + "device_type" : "lamp", + "created_date" : "2020-02-10T23:11:47.487" + }, + { + "serial_number" : "garage_light_on", + "state" : { + "last_status" : "2020-03-30T02:48:45.7501595Z", + "online" : true, + "lamp_state" : "on", + "last_update" : "2020-03-26T15:45:31.4713796Z" + }, + "parent_device_id" : "gateway_serial", + "device_platform" : "myq", + "name" : "Garage Door Light On", + "device_family" : "lamp", + "device_type" : "lamp", + "created_date" : "2020-02-10T23:11:47.487" + } ] } From 0b3f322475c946930244dd504cadf3fd119ec9fd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 Aug 2021 22:02:32 +0200 Subject: [PATCH 243/355] Upgrade pre-commit to 2.14.0 (#54719) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index acfe29db593..d843745cbbf 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,7 +9,7 @@ coverage==5.5 jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.902 -pre-commit==2.13.0 +pre-commit==2.14.0 pylint==2.9.5 pipdeptree==1.0.0 pylint-strict-informational==0.1 From 5e51f57f022c17d6f7b8d4d2b4b08dc101206150 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Aug 2021 15:19:32 -0500 Subject: [PATCH 244/355] Convert nmap_tracker to be a config flow (#54715) --- .coveragerc | 3 +- CODEOWNERS | 1 + .../components/nmap_tracker/__init__.py | 394 +++++++++++++++++- .../components/nmap_tracker/config_flow.py | 215 ++++++++++ .../components/nmap_tracker/const.py | 4 +- .../components/nmap_tracker/device_tracker.py | 261 +++++++----- .../components/nmap_tracker/manifest.json | 12 +- .../components/nmap_tracker/strings.json | 1 - .../nmap_tracker/translations/en.json | 3 +- homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 10 +- requirements_test_all.txt | 7 + tests/components/nmap_tracker/__init__.py | 1 + .../nmap_tracker/test_config_flow.py | 301 +++++++++++++ 14 files changed, 1103 insertions(+), 111 deletions(-) create mode 100644 homeassistant/components/nmap_tracker/config_flow.py create mode 100644 tests/components/nmap_tracker/__init__.py create mode 100644 tests/components/nmap_tracker/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 2cc3bf2d019..4b5c0820650 100644 --- a/.coveragerc +++ b/.coveragerc @@ -697,7 +697,8 @@ omit = homeassistant/components/niko_home_control/light.py homeassistant/components/nilu/air_quality.py homeassistant/components/nissan_leaf/* - homeassistant/components/nmap_tracker/* + homeassistant/components/nmap_tracker/__init__.py + homeassistant/components/nmap_tracker/device_tracker.py homeassistant/components/nmbs/sensor.py homeassistant/components/notion/__init__.py homeassistant/components/notion/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 642de7a04d8..c6696c485fe 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -339,6 +339,7 @@ homeassistant/components/nfandroidtv/* @tkdrob homeassistant/components/nightscout/* @marciogranzotto homeassistant/components/nilu/* @hfurubotten 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/__init__.py b/homeassistant/components/nmap_tracker/__init__.py index da699caaa73..87e9ad895af 100644 --- a/homeassistant/components/nmap_tracker/__init__.py +++ b/homeassistant/components/nmap_tracker/__init__.py @@ -1 +1,393 @@ -"""The nmap_tracker component.""" +"""The Nmap Tracker integration.""" +from __future__ import annotations + +import asyncio +import contextlib +from dataclasses import dataclass +from datetime import datetime, timedelta +from functools import partial +import logging + +import aiohttp +from getmac import get_mac_address +from mac_vendor_lookup import AsyncMacLookup +from nmap import PortScanner, PortScannerError + +from homeassistant.components.device_tracker.const import CONF_SCAN_INTERVAL +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS, EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import CoreState, HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +import homeassistant.util.dt as dt_util + +from .const import ( + CONF_HOME_INTERVAL, + CONF_OPTIONS, + DOMAIN, + NMAP_TRACKED_DEVICES, + PLATFORMS, + TRACKER_SCAN_INTERVAL, +) + +# Some version of nmap will fail with 'Assertion failed: htn.toclock_running == true (Target.cc: stopTimeOutClock: 503)\n' +NMAP_TRANSIENT_FAILURE = "Assertion failed: htn.toclock_running == true" +MAX_SCAN_ATTEMPTS = 16 +OFFLINE_SCANS_TO_MARK_UNAVAILABLE = 3 + + +def short_hostname(hostname): + """Return the first part of the hostname.""" + if hostname is None: + return None + return hostname.split(".")[0] + + +def human_readable_name(hostname, vendor, mac_address): + """Generate a human readable name.""" + if hostname: + return short_hostname(hostname) + if vendor: + return f"{vendor} {mac_address[-8:]}" + return f"Nmap Tracker {mac_address}" + + +@dataclass +class NmapDevice: + """Class for keeping track of an nmap tracked device.""" + + mac_address: str + hostname: str + name: str + ipv4: str + manufacturer: str + reason: str + last_update: datetime.datetime + offline_scans: int + + +class NmapTrackedDevices: + """Storage class for all nmap trackers.""" + + def __init__(self) -> None: + """Initialize the data.""" + self.tracked: dict = {} + self.ipv4_last_mac: dict = {} + self.config_entry_owner: dict = {} + + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Nmap Tracker from a config entry.""" + domain_data = hass.data.setdefault(DOMAIN, {}) + devices = domain_data.setdefault(NMAP_TRACKED_DEVICES, NmapTrackedDevices()) + scanner = domain_data[entry.entry_id] = NmapDeviceScanner(hass, entry, devices) + await scanner.async_setup() + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +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) + + if unload_ok: + _async_untrack_devices(hass, entry) + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +@callback +def _async_untrack_devices(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Remove tracking for devices owned by this config entry.""" + devices = hass.data[DOMAIN][NMAP_TRACKED_DEVICES] + remove_mac_addresses = [ + mac_address + for mac_address, entry_id in devices.config_entry_owner.items() + if entry_id == entry.entry_id + ] + for mac_address in remove_mac_addresses: + if device := devices.tracked.pop(mac_address, None): + devices.ipv4_last_mac.pop(device.ipv4, None) + del devices.config_entry_owner[mac_address] + + +def signal_device_update(mac_address) -> str: + """Signal specific per nmap tracker entry to signal updates in device.""" + return f"{DOMAIN}-device-update-{mac_address}" + + +class NmapDeviceScanner: + """This class scans for devices using nmap.""" + + def __init__(self, hass, entry, devices): + """Initialize the scanner.""" + self.devices = devices + self.home_interval = None + + self._hass = hass + self._entry = entry + + self._scan_lock = None + self._stopping = False + self._scanner = None + + self._entry_id = entry.entry_id + self._hosts = None + self._options = None + self._exclude = None + self._scan_interval = None + + self._known_mac_addresses = {} + self._finished_first_scan = False + self._last_results = [] + self._mac_vendor_lookup = None + + async def async_setup(self): + """Set up the tracker.""" + config = self._entry.options + self._scan_interval = timedelta( + seconds=config.get(CONF_SCAN_INTERVAL, TRACKER_SCAN_INTERVAL) + ) + hosts_list = cv.ensure_list_csv(config[CONF_HOSTS]) + self._hosts = [host for host in hosts_list if host != ""] + excludes_list = cv.ensure_list_csv(config[CONF_EXCLUDE]) + self._exclude = [exclude for exclude in excludes_list if exclude != ""] + self._options = config[CONF_OPTIONS] + self.home_interval = timedelta( + minutes=cv.positive_int(config[CONF_HOME_INTERVAL]) + ) + self._scan_lock = asyncio.Lock() + if self._hass.state == CoreState.running: + await self._async_start_scanner() + return + + self._entry.async_on_unload( + self._hass.bus.async_listen( + EVENT_HOMEASSISTANT_STARTED, self._async_start_scanner + ) + ) + registry = er.async_get(self._hass) + self._known_mac_addresses = { + entry.unique_id: entry.original_name + for entry in registry.entities.values() + if entry.config_entry_id == self._entry_id + } + + @property + def signal_device_new(self) -> str: + """Signal specific per nmap tracker entry to signal new device.""" + return f"{DOMAIN}-device-new-{self._entry_id}" + + @property + def signal_device_missing(self) -> str: + """Signal specific per nmap tracker entry to signal a missing device.""" + return f"{DOMAIN}-device-missing-{self._entry_id}" + + @callback + def _async_get_vendor(self, mac_address): + """Lookup the vendor.""" + oui = self._mac_vendor_lookup.sanitise(mac_address)[:6] + return self._mac_vendor_lookup.prefixes.get(oui) + + @callback + def _async_stop(self): + """Stop the scanner.""" + self._stopping = True + + async def _async_start_scanner(self, *_): + """Start the scanner.""" + self._entry.async_on_unload(self._async_stop) + self._entry.async_on_unload( + async_track_time_interval( + self._hass, + self._async_scan_devices, + self._scan_interval, + ) + ) + self._mac_vendor_lookup = AsyncMacLookup() + with contextlib.suppress((asyncio.TimeoutError, aiohttp.ClientError)): + # We don't care if this fails since it only + # improves the data when we don't have it from nmap + await self._mac_vendor_lookup.load_vendors() + self._hass.async_create_task(self._async_scan_devices()) + + def _build_options(self): + """Build the command line and strip out last results that do not need to be updated.""" + options = self._options + if self.home_interval: + boundary = dt_util.now() - self.home_interval + last_results = [ + device for device in self._last_results if device.last_update > boundary + ] + if last_results: + exclude_hosts = self._exclude + [device.ipv4 for device in last_results] + else: + exclude_hosts = self._exclude + else: + last_results = [] + exclude_hosts = self._exclude + if exclude_hosts: + options += f" --exclude {','.join(exclude_hosts)}" + # Report reason + if "--reason" not in options: + options += " --reason" + # Report down hosts + if "-v" not in options: + options += " -v" + self._last_results = last_results + return options + + async def _async_scan_devices(self, *_): + """Scan devices and dispatch.""" + if self._scan_lock.locked(): + _LOGGER.debug( + "Nmap scanning is taking longer than the scheduled interval: %s", + TRACKER_SCAN_INTERVAL, + ) + return + + async with self._scan_lock: + try: + await self._async_run_nmap_scan() + except PortScannerError as ex: + _LOGGER.error("Nmap scanning failed: %s", ex) + + if not self._finished_first_scan: + self._finished_first_scan = True + await self._async_mark_missing_devices_as_not_home() + + async def _async_mark_missing_devices_as_not_home(self): + # After all config entries have finished their first + # scan we mark devices that were not found as not_home + # from unavailable + now = dt_util.now() + for mac_address, original_name in self._known_mac_addresses.items(): + if mac_address in self.devices.tracked: + continue + self.devices.config_entry_owner[mac_address] = self._entry_id + self.devices.tracked[mac_address] = NmapDevice( + mac_address, + None, + original_name, + None, + self._async_get_vendor(mac_address), + "Device not found in initial scan", + now, + 1, + ) + async_dispatcher_send(self._hass, self.signal_device_missing, mac_address) + + def _run_nmap_scan(self): + """Run nmap and return the result.""" + options = self._build_options() + if not self._scanner: + self._scanner = PortScanner() + _LOGGER.debug("Scanning %s with args: %s", self._hosts, options) + for attempt in range(MAX_SCAN_ATTEMPTS): + try: + result = self._scanner.scan( + hosts=" ".join(self._hosts), + arguments=options, + timeout=TRACKER_SCAN_INTERVAL * 10, + ) + break + except PortScannerError as ex: + if attempt < (MAX_SCAN_ATTEMPTS - 1) and NMAP_TRANSIENT_FAILURE in str( + ex + ): + _LOGGER.debug("Nmap saw transient error %s", NMAP_TRANSIENT_FAILURE) + continue + raise + _LOGGER.debug( + "Finished scanning %s with args: %s", + self._hosts, + options, + ) + return result + + @callback + def _async_increment_device_offline(self, ipv4, reason): + """Mark an IP offline.""" + if not (formatted_mac := self.devices.ipv4_last_mac.get(ipv4)): + return + if not (device := self.devices.tracked.get(formatted_mac)): + # Device was unloaded + return + device.offline_scans += 1 + if device.offline_scans < OFFLINE_SCANS_TO_MARK_UNAVAILABLE: + return + device.reason = reason + async_dispatcher_send(self._hass, signal_device_update(formatted_mac), False) + del self.devices.ipv4_last_mac[ipv4] + + async def _async_run_nmap_scan(self): + """Scan the network for devices and dispatch events.""" + result = await self._hass.async_add_executor_job(self._run_nmap_scan) + if self._stopping: + return + + devices = self.devices + entry_id = self._entry_id + now = dt_util.now() + for ipv4, info in result["scan"].items(): + status = info["status"] + reason = status["reason"] + if status["state"] != "up": + self._async_increment_device_offline(ipv4, reason) + continue + # Mac address only returned if nmap ran as root + mac = info["addresses"].get( + "mac" + ) or await self._hass.async_add_executor_job( + partial(get_mac_address, ip=ipv4) + ) + if mac is None: + self._async_increment_device_offline(ipv4, "No MAC address found") + _LOGGER.info("No MAC address found for %s", ipv4) + continue + + formatted_mac = format_mac(mac) + new = formatted_mac not in devices.tracked + if ( + new + and formatted_mac not in devices.tracked + and formatted_mac not in self._known_mac_addresses + ): + continue + + if ( + devices.config_entry_owner.setdefault(formatted_mac, entry_id) + != entry_id + ): + continue + + hostname = info["hostnames"][0]["name"] if info["hostnames"] else ipv4 + vendor = info.get("vendor", {}).get(mac) or self._async_get_vendor(mac) + name = human_readable_name(hostname, vendor, mac) + device = NmapDevice( + formatted_mac, hostname, name, ipv4, vendor, reason, now, 0 + ) + + devices.tracked[formatted_mac] = device + devices.ipv4_last_mac[ipv4] = formatted_mac + self._last_results.append(device) + + if new: + async_dispatcher_send(self._hass, self.signal_device_new, formatted_mac) + else: + async_dispatcher_send( + self._hass, signal_device_update(formatted_mac), True + ) diff --git a/homeassistant/components/nmap_tracker/config_flow.py b/homeassistant/components/nmap_tracker/config_flow.py new file mode 100644 index 00000000000..eaea87e775a --- /dev/null +++ b/homeassistant/components/nmap_tracker/config_flow.py @@ -0,0 +1,215 @@ +"""Config flow for Nmap Tracker integration.""" +from __future__ import annotations + +from ipaddress import ip_address, ip_network, summarize_address_range +from typing import Any + +import ifaddr +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.device_tracker.const import CONF_SCAN_INTERVAL +from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv +from homeassistant.util import get_local_ip + +from .const import ( + CONF_HOME_INTERVAL, + CONF_OPTIONS, + DEFAULT_OPTIONS, + DOMAIN, + TRACKER_SCAN_INTERVAL, +) + +DEFAULT_NETWORK_PREFIX = 24 + + +def get_network(): + """Search adapters for the network.""" + adapters = ifaddr.get_adapters() + local_ip = get_local_ip() + network_prefix = ( + get_ip_prefix_from_adapters(local_ip, adapters) or DEFAULT_NETWORK_PREFIX + ) + return str(ip_network(f"{local_ip}/{network_prefix}", False)) + + +def get_ip_prefix_from_adapters(local_ip, adapters): + """Find the network prefix for an adapter.""" + for adapter in adapters: + for ip_cfg in adapter.ips: + if local_ip == ip_cfg.ip: + return ip_cfg.network_prefix + + +def _normalize_ips_and_network(hosts_str): + """Check if a list of hosts are all ips or ip networks.""" + + normalized_hosts = [] + hosts = [host for host in cv.ensure_list_csv(hosts_str) if host != ""] + + for host in sorted(hosts): + try: + start, end = host.split("-", 1) + if "." not in end: + ip_1, ip_2, ip_3, _ = start.split(".", 3) + end = ".".join([ip_1, ip_2, ip_3, end]) + summarize_address_range(ip_address(start), ip_address(end)) + except ValueError: + pass + else: + normalized_hosts.append(host) + continue + + try: + ip_addr = ip_address(host) + except ValueError: + pass + else: + normalized_hosts.append(str(ip_addr)) + continue + + try: + network = ip_network(host) + except ValueError: + return None + else: + normalized_hosts.append(str(network)) + + return normalized_hosts + + +def normalize_input(user_input): + """Validate hosts and exclude are valid.""" + errors = {} + normalized_hosts = _normalize_ips_and_network(user_input[CONF_HOSTS]) + if not normalized_hosts: + errors[CONF_HOSTS] = "invalid_hosts" + else: + user_input[CONF_HOSTS] = ",".join(normalized_hosts) + + normalized_exclude = _normalize_ips_and_network(user_input[CONF_EXCLUDE]) + if normalized_exclude is None: + errors[CONF_EXCLUDE] = "invalid_hosts" + else: + user_input[CONF_EXCLUDE] = ",".join(normalized_exclude) + + return errors + + +async def _async_build_schema_with_user_input(hass, user_input, include_options): + hosts = user_input.get(CONF_HOSTS, await hass.async_add_executor_job(get_network)) + exclude = user_input.get( + CONF_EXCLUDE, await hass.async_add_executor_job(get_local_ip) + ) + schema = { + vol.Required(CONF_HOSTS, default=hosts): str, + vol.Required( + CONF_HOME_INTERVAL, default=user_input.get(CONF_HOME_INTERVAL, 0) + ): int, + vol.Optional(CONF_EXCLUDE, default=exclude): str, + vol.Optional( + CONF_OPTIONS, default=user_input.get(CONF_OPTIONS, DEFAULT_OPTIONS) + ): str, + } + if include_options: + schema.update( + { + vol.Optional( + CONF_SCAN_INTERVAL, + default=user_input.get(CONF_SCAN_INTERVAL, TRACKER_SCAN_INTERVAL), + ): vol.All(vol.Coerce(int), vol.Range(min=10, max=3600)), + } + ) + return vol.Schema(schema) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for homekit.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.options = dict(config_entry.options) + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + errors = {} + if user_input is not None: + errors = normalize_input(user_input) + self.options.update(user_input) + + if not errors: + return self.async_create_entry( + title=f"Nmap Tracker {self.options[CONF_HOSTS]}", data=self.options + ) + + return self.async_show_form( + step_id="init", + data_schema=await _async_build_schema_with_user_input( + self.hass, self.options, True + ), + errors=errors, + ) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Nmap Tracker.""" + + VERSION = 1 + + def __init__(self): + """Initialize config flow.""" + self.options = {} + + 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: + if not self._async_is_unique_host_list(user_input): + return self.async_abort(reason="already_configured") + + errors = normalize_input(user_input) + self.options.update(user_input) + + if not errors: + return self.async_create_entry( + title=f"Nmap Tracker {user_input[CONF_HOSTS]}", + data={}, + options=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=await _async_build_schema_with_user_input( + self.hass, self.options, False + ), + errors=errors, + ) + + def _async_is_unique_host_list(self, user_input): + hosts = _normalize_ips_and_network(user_input[CONF_HOSTS]) + for entry in self._async_current_entries(): + if _normalize_ips_and_network(entry.options[CONF_HOSTS]) == hosts: + return False + return True + + async def async_step_import(self, user_input=None): + """Handle import from yaml.""" + if not self._async_is_unique_host_list(user_input): + return self.async_abort(reason="already_configured") + + normalize_input(user_input) + + return self.async_create_entry( + title=f"Nmap Tracker {user_input[CONF_HOSTS]}", data={}, options=user_input + ) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) diff --git a/homeassistant/components/nmap_tracker/const.py b/homeassistant/components/nmap_tracker/const.py index 88118a81811..f8b467d2f19 100644 --- a/homeassistant/components/nmap_tracker/const.py +++ b/homeassistant/components/nmap_tracker/const.py @@ -9,8 +9,6 @@ NMAP_TRACKED_DEVICES = "nmap_tracked_devices" # Interval in minutes to exclude devices from a scan while they are home CONF_HOME_INTERVAL = "home_interval" CONF_OPTIONS = "scan_options" -DEFAULT_OPTIONS = "-F --host-timeout 5s" +DEFAULT_OPTIONS = "-F -T4 --min-rate 10 --host-timeout 5s" TRACKER_SCAN_INTERVAL = 120 - -DEFAULT_TRACK_NEW_DEVICES = True diff --git a/homeassistant/components/nmap_tracker/device_tracker.py b/homeassistant/components/nmap_tracker/device_tracker.py index 69c65873e51..fcf9ae6189e 100644 --- a/homeassistant/components/nmap_tracker/device_tracker.py +++ b/homeassistant/components/nmap_tracker/device_tracker.py @@ -1,29 +1,35 @@ """Support for scanning a network with nmap.""" -from collections import namedtuple -from datetime import timedelta -import logging -from getmac import get_mac_address -from nmap import PortScanner, PortScannerError +import logging +from typing import Callable + import voluptuous as vol from homeassistant.components.device_tracker import ( - DOMAIN, + DOMAIN as DEVICE_TRACKER_DOMAIN, PLATFORM_SCHEMA, - DeviceScanner, + SOURCE_TYPE_ROUTER, ) +from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.components.device_tracker.const import CONF_SCAN_INTERVAL +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import NmapDeviceScanner, short_hostname, signal_device_update +from .const import ( + CONF_HOME_INTERVAL, + CONF_OPTIONS, + DEFAULT_OPTIONS, + DOMAIN, + TRACKER_SCAN_INTERVAL, +) _LOGGER = logging.getLogger(__name__) -# Interval in minutes to exclude devices from a scan while they are home -CONF_HOME_INTERVAL = "home_interval" -CONF_OPTIONS = "scan_options" -DEFAULT_OPTIONS = "-F --host-timeout 5s" - - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOSTS): cv.ensure_list, @@ -34,100 +40,161 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def get_scanner(hass, config): +async def async_get_scanner(hass, config): """Validate the configuration and return a Nmap scanner.""" - return NmapDeviceScanner(config[DOMAIN]) + validated_config = config[DEVICE_TRACKER_DOMAIN] + if CONF_SCAN_INTERVAL in validated_config: + scan_interval = validated_config[CONF_SCAN_INTERVAL].total_seconds() + else: + scan_interval = TRACKER_SCAN_INTERVAL -Device = namedtuple("Device", ["mac", "name", "ip", "last_update"]) + import_config = { + CONF_HOSTS: ",".join(validated_config[CONF_HOSTS]), + CONF_HOME_INTERVAL: validated_config[CONF_HOME_INTERVAL], + CONF_EXCLUDE: ",".join(validated_config[CONF_EXCLUDE]), + CONF_OPTIONS: validated_config[CONF_OPTIONS], + CONF_SCAN_INTERVAL: scan_interval, + } - -class NmapDeviceScanner(DeviceScanner): - """This class scans for devices using nmap.""" - - exclude = [] - - def __init__(self, config): - """Initialize the scanner.""" - self.last_results = [] - - self.hosts = config[CONF_HOSTS] - self.exclude = config[CONF_EXCLUDE] - minutes = config[CONF_HOME_INTERVAL] - self._options = config[CONF_OPTIONS] - self.home_interval = timedelta(minutes=minutes) - - _LOGGER.debug("Scanner initialized") - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - - _LOGGER.debug("Nmap last results %s", self.last_results) - - return [device.mac for device in self.last_results] - - def get_device_name(self, device): - """Return the name of the given device or None if we don't know.""" - filter_named = [ - result.name for result in self.last_results if result.mac == device - ] - - if filter_named: - return filter_named[0] - return None - - def get_extra_attributes(self, device): - """Return the IP of the given device.""" - filter_ip = next( - (result.ip for result in self.last_results if result.mac == device), None + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=import_config, ) - return {"ip": filter_ip} + ) - def _update_info(self): - """Scan the network for devices. + _LOGGER.warning( + "Your Nmap Tracker configuration has been imported into the UI, " + "please remove it from configuration.yaml. " + ) - Returns boolean if scanning successful. - """ - _LOGGER.debug("Scanning") - scanner = PortScanner() +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up device tracker for Nmap Tracker component.""" + nmap_tracker = hass.data[DOMAIN][entry.entry_id] - options = self._options + @callback + def device_new(mac_address): + """Signal a new device.""" + async_add_entities([NmapTrackerEntity(nmap_tracker, mac_address, True)]) - if self.home_interval: - boundary = dt_util.now() - self.home_interval - last_results = [ - device for device in self.last_results if device.last_update > boundary - ] - if last_results: - exclude_hosts = self.exclude + [device.ip for device in last_results] - else: - exclude_hosts = self.exclude - else: - last_results = [] - exclude_hosts = self.exclude - if exclude_hosts: - options += f" --exclude {','.join(exclude_hosts)}" + @callback + def device_missing(mac_address): + """Signal a missing device.""" + async_add_entities([NmapTrackerEntity(nmap_tracker, mac_address, False)]) - try: - result = scanner.scan(hosts=" ".join(self.hosts), arguments=options) - except PortScannerError: - return False + entry.async_on_unload( + async_dispatcher_connect(hass, nmap_tracker.signal_device_new, device_new) + ) + entry.async_on_unload( + async_dispatcher_connect( + hass, nmap_tracker.signal_device_missing, device_missing + ) + ) - now = dt_util.now() - for ipv4, info in result["scan"].items(): - if info["status"]["state"] != "up": - continue - name = info["hostnames"][0]["name"] if info["hostnames"] else ipv4 - # Mac address only returned if nmap ran as root - mac = info["addresses"].get("mac") or get_mac_address(ip=ipv4) - if mac is None: - _LOGGER.info("No MAC address found for %s", ipv4) - continue - last_results.append(Device(mac.upper(), name, ipv4, now)) - self.last_results = last_results +class NmapTrackerEntity(ScannerEntity): + """An Nmap Tracker entity.""" - _LOGGER.debug("nmap scan successful") - return True + def __init__( + self, nmap_tracker: NmapDeviceScanner, mac_address: str, active: bool + ) -> None: + """Initialize an nmap tracker entity.""" + self._mac_address = mac_address + self._nmap_tracker = nmap_tracker + self._tracked = self._nmap_tracker.devices.tracked + self._active = active + + @property + def _device(self) -> bool: + """Get latest device state.""" + return self._tracked[self._mac_address] + + @property + def is_connected(self) -> bool: + """Return device status.""" + return self._active + + @property + def name(self) -> str: + """Return device name.""" + return self._device.name + + @property + def unique_id(self) -> str: + """Return device unique id.""" + return self._mac_address + + @property + def ip_address(self) -> str: + """Return the primary ip address of the device.""" + return self._device.ipv4 + + @property + def mac_address(self) -> str: + """Return the mac address of the device.""" + return self._mac_address + + @property + def hostname(self) -> str: + """Return hostname of the device.""" + return short_hostname(self._device.hostname) + + @property + def source_type(self) -> str: + """Return tracker source type.""" + return SOURCE_TYPE_ROUTER + + @property + def device_info(self): + """Return the device information.""" + return { + "connections": {(CONNECTION_NETWORK_MAC, self._mac_address)}, + "default_manufacturer": self._device.manufacturer, + "default_name": self.name, + } + + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False + + @property + def icon(self): + """Return device icon.""" + return "mdi:lan-connect" if self._active else "mdi:lan-disconnect" + + @callback + def async_process_update(self, online: bool) -> None: + """Update device.""" + self._active = online + + @property + def extra_state_attributes(self): + """Return the attributes.""" + return { + "last_time_reachable": self._device.last_update.isoformat( + timespec="seconds" + ), + "reason": self._device.reason, + } + + @callback + def async_on_demand_update(self, online: bool): + """Update state.""" + self.async_process_update(online) + self.async_write_ha_state() + + async def async_added_to_hass(self): + """Register state update callback.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + signal_device_update(self._mac_address), + self.async_on_demand_update, + ) + ) diff --git a/homeassistant/components/nmap_tracker/manifest.json b/homeassistant/components/nmap_tracker/manifest.json index 9f81c0facaf..ee05843c4fe 100644 --- a/homeassistant/components/nmap_tracker/manifest.json +++ b/homeassistant/components/nmap_tracker/manifest.json @@ -2,7 +2,13 @@ "domain": "nmap_tracker", "name": "Nmap Tracker", "documentation": "https://www.home-assistant.io/integrations/nmap_tracker", - "requirements": ["python-nmap==0.6.1", "getmac==0.8.2"], - "codeowners": [], - "iot_class": "local_polling" + "requirements": [ + "netmap==0.7.0.2", + "getmac==0.8.2", + "ifaddr==0.1.7", + "mac-vendor-lookup==0.1.11" + ], + "codeowners": ["@bdraco"], + "iot_class": "local_polling", + "config_flow": true } diff --git a/homeassistant/components/nmap_tracker/strings.json b/homeassistant/components/nmap_tracker/strings.json index ecb470a6f0d..d42e1067503 100644 --- a/homeassistant/components/nmap_tracker/strings.json +++ b/homeassistant/components/nmap_tracker/strings.json @@ -9,7 +9,6 @@ "home_interval": "[%key:component::nmap_tracker::config::step::user::data::home_interval%]", "exclude": "[%key:component::nmap_tracker::config::step::user::data::exclude%]", "scan_options": "[%key:component::nmap_tracker::config::step::user::data::scan_options%]", - "track_new_devices": "Track new devices", "interval_seconds": "Scan interval" } } diff --git a/homeassistant/components/nmap_tracker/translations/en.json b/homeassistant/components/nmap_tracker/translations/en.json index 6b83532a0e2..985225414a6 100644 --- a/homeassistant/components/nmap_tracker/translations/en.json +++ b/homeassistant/components/nmap_tracker/translations/en.json @@ -29,8 +29,7 @@ "home_interval": "Minimum number of minutes between scans of active devices (preserve battery)", "hosts": "Network addresses (comma seperated) to scan", "interval_seconds": "Scan interval", - "scan_options": "Raw configurable scan options for Nmap", - "track_new_devices": "Track new devices" + "scan_options": "Raw configurable scan options for Nmap" }, "description": "Configure hosts to be scanned by Nmap. Network address and excludes can be IP Addresses (192.168.1.1), IP Networks (192.168.0.0/24) or IP Ranges (192.168.1.0-32)." } diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d125f507d3a..b4a6fcc3775 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -180,6 +180,7 @@ FLOWS = [ "nexia", "nfandroidtv", "nightscout", + "nmap_tracker", "notion", "nuheat", "nuki", diff --git a/requirements_all.txt b/requirements_all.txt index 13018dcc9b7..2a03e1c4855 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -840,6 +840,7 @@ ibmiotf==0.3.4 icmplib==3.0 # homeassistant.components.network +# homeassistant.components.nmap_tracker ifaddr==0.1.7 # homeassistant.components.iglo @@ -941,6 +942,9 @@ lw12==0.9.2 # homeassistant.components.lyft lyft_rides==0.2 +# homeassistant.components.nmap_tracker +mac-vendor-lookup==0.1.11 + # homeassistant.components.magicseaweed magicseaweed==1.0.3 @@ -1019,6 +1023,9 @@ netdata==0.2.0 # homeassistant.components.discovery netdisco==2.9.0 +# homeassistant.components.nmap_tracker +netmap==0.7.0.2 + # homeassistant.components.nam nettigo-air-monitor==1.0.0 @@ -1871,9 +1878,6 @@ python-mystrom==1.1.2 # homeassistant.components.nest python-nest==4.1.0 -# homeassistant.components.nmap_tracker -python-nmap==0.6.1 - # homeassistant.components.ozw python-openzwave-mqtt[mqtt-client]==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 09b17f6415e..706dcf1da77 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -486,6 +486,7 @@ iaqualink==0.3.90 icmplib==3.0 # homeassistant.components.network +# homeassistant.components.nmap_tracker ifaddr==0.1.7 # homeassistant.components.influxdb @@ -527,6 +528,9 @@ logi_circle==0.2.2 # homeassistant.components.luftdaten luftdaten==0.6.5 +# homeassistant.components.nmap_tracker +mac-vendor-lookup==0.1.11 + # homeassistant.components.maxcube maxcube-api==0.4.3 @@ -575,6 +579,9 @@ nessclient==0.9.15 # homeassistant.components.discovery netdisco==2.9.0 +# homeassistant.components.nmap_tracker +netmap==0.7.0.2 + # homeassistant.components.nam nettigo-air-monitor==1.0.0 diff --git a/tests/components/nmap_tracker/__init__.py b/tests/components/nmap_tracker/__init__.py new file mode 100644 index 00000000000..f5e0c85df31 --- /dev/null +++ b/tests/components/nmap_tracker/__init__.py @@ -0,0 +1 @@ +"""Tests for the Nmap Tracker integration.""" diff --git a/tests/components/nmap_tracker/test_config_flow.py b/tests/components/nmap_tracker/test_config_flow.py new file mode 100644 index 00000000000..6365dd7407a --- /dev/null +++ b/tests/components/nmap_tracker/test_config_flow.py @@ -0,0 +1,301 @@ +"""Test the Nmap Tracker config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.device_tracker.const import CONF_SCAN_INTERVAL +from homeassistant.components.nmap_tracker.const import ( + CONF_HOME_INTERVAL, + CONF_OPTIONS, + DEFAULT_OPTIONS, + DOMAIN, +) +from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS +from homeassistant.core import CoreState, HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + "hosts", ["1.1.1.1", "192.168.1.0/24", "192.168.1.0/24,192.168.2.0/24"] +) +async def test_form(hass: HomeAssistant, hosts: str) -> 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"] == "form" + assert result["errors"] == {} + + schema_defaults = result["data_schema"]({}) + assert CONF_SCAN_INTERVAL not in schema_defaults + + with patch( + "homeassistant.components.nmap_tracker.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOSTS: hosts, + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == f"Nmap Tracker {hosts}" + assert result2["data"] == {} + assert result2["options"] == { + CONF_HOSTS: hosts, + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_range(hass: HomeAssistant) -> None: + """Test we get the form and can take an ip range.""" + 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"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.nmap_tracker.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOSTS: "192.168.0.5-12", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Nmap Tracker 192.168.0.5-12" + assert result2["data"] == {} + assert result2["options"] == { + CONF_HOSTS: "192.168.0.5-12", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_hosts(hass: HomeAssistant) -> None: + """Test invalid hosts passed in.""" + 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"] == "form" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOSTS: "not an ip block", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["errors"] == {CONF_HOSTS: "invalid_hosts"} + + +async def test_form_already_configured(hass: HomeAssistant) -> None: + """Test duplicate host list.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + config_entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + CONF_HOSTS: "192.168.0.0/20", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4", + }, + ) + config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOSTS: "192.168.0.0/20", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" + + +async def test_form_invalid_excludes(hass: HomeAssistant) -> None: + """Test invalid excludes passed in.""" + 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"] == "form" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOSTS: "3.3.3.3", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "not an exclude", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["errors"] == {CONF_EXCLUDE: "invalid_hosts"} + + +async def test_options_flow(hass: HomeAssistant) -> None: + """Test we can edit options.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + CONF_HOSTS: "192.168.1.0/24", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4", + }, + ) + config_entry.add_to_hass(hass) + hass.state = CoreState.stopped + + 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) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + assert result["data_schema"]({}) == { + CONF_EXCLUDE: "4.4.4.4", + CONF_HOME_INTERVAL: 3, + CONF_HOSTS: "192.168.1.0/24", + CONF_SCAN_INTERVAL: 120, + CONF_OPTIONS: "-F -T4 --min-rate 10 --host-timeout 5s", + } + + with patch( + "homeassistant.components.nmap_tracker.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_HOSTS: "192.168.1.0/24, 192.168.2.0/24", + CONF_HOME_INTERVAL: 5, + CONF_OPTIONS: "-sn", + CONF_EXCLUDE: "4.4.4.4, 5.5.5.5", + CONF_SCAN_INTERVAL: 10, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + CONF_HOSTS: "192.168.1.0/24,192.168.2.0/24", + CONF_HOME_INTERVAL: 5, + CONF_OPTIONS: "-sn", + CONF_EXCLUDE: "4.4.4.4,5.5.5.5", + CONF_SCAN_INTERVAL: 10, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import(hass: HomeAssistant) -> None: + """Test we can import from yaml.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( + "homeassistant.components.nmap_tracker.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_HOSTS: "1.2.3.4/20", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4, 6.4.3.2", + CONF_SCAN_INTERVAL: 2000, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == "Nmap Tracker 1.2.3.4/20" + assert result["data"] == {} + assert result["options"] == { + CONF_HOSTS: "1.2.3.4/20", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4,6.4.3.2", + CONF_SCAN_INTERVAL: 2000, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_aborts_if_matching(hass: HomeAssistant) -> None: + """Test we can import from yaml.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + CONF_HOSTS: "192.168.0.0/20", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4", + }, + ) + config_entry.add_to_hass(hass) + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_HOSTS: "192.168.0.0/20", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4, 6.4.3.2", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" From bb4a36c8772ba139a26714ce13d5be29118b8921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 16 Aug 2021 23:47:37 +0300 Subject: [PATCH 245/355] Upgrade mypy to 0.910 and types-* (#54574) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Daniel Hjelseth Høyer --- requirements_test.txt | 33 ++++++++++++++++++--------------- script/hassfest/mypy_config.py | 4 +++- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index d843745cbbf..63e102ec77e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -2,13 +2,16 @@ # make new things fail. Manually update these pins when pulling in a # new version +# types-* that have versions roughly corresponding to the packages they +# contain hints for available should be kept in sync with them + -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt codecov==2.1.12 coverage==5.5 jsonpickle==1.4.1 mock-open==1.4.0 -mypy==0.902 +mypy==0.910 pre-commit==2.14.0 pylint==2.9.5 pipdeptree==1.0.0 @@ -25,19 +28,19 @@ responses==0.12.0 respx==0.17.0 stdlib-list==0.7.0 tqdm==4.49.0 -types-backports==0.1.2 -types-certifi==0.1.3 -types-chardet==0.1.2 +types-backports==0.1.3 +types-certifi==0.1.4 +types-chardet==0.1.5 types-cryptography==3.3.2 -types-decorator==0.1.4 -types-emoji==1.2.1 -types-enum34==0.1.5 -types-ipaddress==0.1.2 +types-decorator==0.1.7 +types-emoji==1.2.4 +types-enum34==0.1.8 +types-ipaddress==0.1.5 types-jwt==0.1.3 -types-pkg-resources==0.1.2 -types-python-slugify==0.1.0 -types-pytz==0.1.1 -types-PyYAML==5.4.1 -types-requests==0.1.11 -types-toml==0.1.2 -types-ujson==0.1.0 +types-pkg-resources==0.1.3 +types-python-slugify==0.1.2 +types-pytz==2021.1.2 +types-PyYAML==5.4.6 +types-requests==2.25.1 +types-toml==0.1.5 +types-ujson==0.1.1 diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 1b24a935084..a69a9ec8d88 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -195,7 +195,9 @@ GENERAL_SETTINGS: Final[dict[str, str]] = { } # This is basically the list of checks which is enabled for "strict=true". -# But "strict=true" is applied globally, so we need to list all checks manually. +# "strict=false" in config files does not turn strict settings off if they've been +# set in a more general section (it instead means as if strict was not specified at +# all), so we need to list all checks manually to be able to flip them wholesale. STRICT_SETTINGS: Final[list[str]] = [ "check_untyped_defs", "disallow_incomplete_defs", From f9fbcd4aec7192cc0763aac1dc7d9eac0131501f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 16 Aug 2021 22:52:47 +0200 Subject: [PATCH 246/355] Use EntityDescription - qbittorrent (#54428) --- .../components/qbittorrent/sensor.py | 98 ++++++++++--------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index 5f57cd19cfe..4663b203248 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -1,11 +1,17 @@ """Support for monitoring the qBittorrent API.""" +from __future__ import annotations + import logging from qbittorrent.client import Client, LoginRequired from requests.exceptions import RequestException import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, @@ -25,11 +31,22 @@ SENSOR_TYPE_UPLOAD_SPEED = "upload_speed" DEFAULT_NAME = "qBittorrent" -SENSOR_TYPES = { - SENSOR_TYPE_CURRENT_STATUS: ["Status", None], - SENSOR_TYPE_DOWNLOAD_SPEED: ["Down Speed", DATA_RATE_KILOBYTES_PER_SECOND], - SENSOR_TYPE_UPLOAD_SPEED: ["Up Speed", DATA_RATE_KILOBYTES_PER_SECOND], -} +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key=SENSOR_TYPE_CURRENT_STATUS, + name="Status", + ), + SensorEntityDescription( + key=SENSOR_TYPE_DOWNLOAD_SPEED, + name="Down Speed", + native_unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, + ), + SensorEntityDescription( + key=SENSOR_TYPE_UPLOAD_SPEED, + name="Up Speed", + native_unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, + ), +) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -56,12 +73,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = config.get(CONF_NAME) - dev = [] - for sensor_type in SENSOR_TYPES: - sensor = QBittorrentSensor(sensor_type, client, name, LoginRequired) - dev.append(sensor) + entities = [ + QBittorrentSensor(description, client, name, LoginRequired) + for description in SENSOR_TYPES + ] - add_entities(dev, True) + add_entities(entities, True) def format_speed(speed): @@ -73,45 +90,29 @@ def format_speed(speed): class QBittorrentSensor(SensorEntity): """Representation of an qBittorrent sensor.""" - def __init__(self, sensor_type, qbittorrent_client, client_name, exception): + def __init__( + self, + description: SensorEntityDescription, + qbittorrent_client, + client_name, + exception, + ): """Initialize the qBittorrent sensor.""" - self._name = SENSOR_TYPES[sensor_type][0] + self.entity_description = description self.client = qbittorrent_client - self.type = sensor_type - self.client_name = client_name - self._state = None - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] - self._available = False self._exception = exception - @property - def name(self): - """Return the name of the sensor.""" - return f"{self.client_name} {self._name}" - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def available(self): - """Return true if device is available.""" - return self._available - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement + self._attr_name = f"{client_name} {description.name}" + self._attr_available = False def update(self): """Get the latest data from qBittorrent and updates the state.""" try: data = self.client.sync_main_data() - self._available = True + self._attr_available = True except RequestException: _LOGGER.error("Connection lost") - self._available = False + self._attr_available = False return except self._exception: _LOGGER.error("Invalid authentication") @@ -123,17 +124,18 @@ class QBittorrentSensor(SensorEntity): download = data["server_state"]["dl_info_speed"] upload = data["server_state"]["up_info_speed"] - if self.type == SENSOR_TYPE_CURRENT_STATUS: + sensor_type = self.entity_description.key + if sensor_type == SENSOR_TYPE_CURRENT_STATUS: if upload > 0 and download > 0: - self._state = "up_down" + self._attr_native_value = "up_down" elif upload > 0 and download == 0: - self._state = "seeding" + self._attr_native_value = "seeding" elif upload == 0 and download > 0: - self._state = "downloading" + self._attr_native_value = "downloading" else: - self._state = STATE_IDLE + self._attr_native_value = STATE_IDLE - elif self.type == SENSOR_TYPE_DOWNLOAD_SPEED: - self._state = format_speed(download) - elif self.type == SENSOR_TYPE_UPLOAD_SPEED: - self._state = format_speed(upload) + elif sensor_type == SENSOR_TYPE_DOWNLOAD_SPEED: + self._attr_native_value = format_speed(download) + elif sensor_type == SENSOR_TYPE_UPLOAD_SPEED: + self._attr_native_value = format_speed(upload) From 236ccb933c4664804496de54a8a7df732a7ef163 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 16 Aug 2021 22:54:56 +0200 Subject: [PATCH 247/355] Use EntityDescription - point (#54363) --- homeassistant/components/point/sensor.py | 87 +++++++++++++++++------- 1 file changed, 62 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/point/sensor.py b/homeassistant/components/point/sensor.py index 87981d7b29e..8d4ee69fca2 100644 --- a/homeassistant/components/point/sensor.py +++ b/homeassistant/components/point/sensor.py @@ -1,7 +1,14 @@ """Support for Minut Point sensors.""" +from __future__ import annotations + +from dataclasses import dataclass import logging -from homeassistant.components.sensor import DOMAIN, SensorEntity +from homeassistant.components.sensor import ( + DOMAIN, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, @@ -21,12 +28,48 @@ _LOGGER = logging.getLogger(__name__) DEVICE_CLASS_SOUND = "sound_level" -SENSOR_TYPES = { - DEVICE_CLASS_TEMPERATURE: (None, 1, TEMP_CELSIUS), - DEVICE_CLASS_PRESSURE: (None, 0, PRESSURE_HPA), - DEVICE_CLASS_HUMIDITY: (None, 1, PERCENTAGE), - DEVICE_CLASS_SOUND: ("mdi:ear-hearing", 1, SOUND_PRESSURE_WEIGHTED_DBA), -} + +@dataclass +class MinutPointRequiredKeysMixin: + """Mixin for required keys.""" + + precision: int + + +@dataclass +class MinutPointSensorEntityDescription( + SensorEntityDescription, MinutPointRequiredKeysMixin +): + """Describes MinutPoint sensor entity.""" + + +SENSOR_TYPES: tuple[MinutPointSensorEntityDescription, ...] = ( + MinutPointSensorEntityDescription( + key="temperature", + precision=1, + device_class=DEVICE_CLASS_TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + ), + MinutPointSensorEntityDescription( + key="pressure", + precision=0, + device_class=DEVICE_CLASS_PRESSURE, + native_unit_of_measurement=PRESSURE_HPA, + ), + MinutPointSensorEntityDescription( + key="humidity", + precision=1, + device_class=DEVICE_CLASS_HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + ), + MinutPointSensorEntityDescription( + key="sound", + precision=1, + device_class=DEVICE_CLASS_SOUND, + icon="mdi:ear-hearing", + native_unit_of_measurement=SOUND_PRESSURE_WEIGHTED_DBA, + ), +) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -36,10 +79,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Discover and add a discovered sensor.""" client = hass.data[POINT_DOMAIN][config_entry.entry_id] async_add_entities( - ( - MinutPointSensor(client, device_id, sensor_type) - for sensor_type in SENSOR_TYPES - ), + [ + MinutPointSensor(client, device_id, description) + for description in SENSOR_TYPES + ], True, ) @@ -51,10 +94,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class MinutPointSensor(MinutPointEntity, SensorEntity): """The platform class required by Home Assistant.""" - def __init__(self, point_client, device_id, device_class): + entity_description: MinutPointSensorEntityDescription + + def __init__( + self, point_client, device_id, description: MinutPointSensorEntityDescription + ): """Initialize the sensor.""" - super().__init__(point_client, device_id, device_class) - self._device_prop = SENSOR_TYPES[device_class] + super().__init__(point_client, device_id, description.device_class) + self.entity_description = description async def _update_callback(self): """Update the value of the sensor.""" @@ -64,19 +111,9 @@ class MinutPointSensor(MinutPointEntity, SensorEntity): self._updated = parse_datetime(self.device.last_update) self.async_write_ha_state() - @property - def icon(self): - """Return the icon representation.""" - return self._device_prop[0] - @property def native_value(self): """Return the state of the sensor.""" if self.value is None: return None - return round(self.value, self._device_prop[1]) - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement.""" - return self._device_prop[2] + return round(self.value, self.entity_description.precision) From b72ed68d61efdd8b52d17a27d2c2768a01959a4c Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 16 Aug 2021 22:55:52 +0200 Subject: [PATCH 248/355] Activate mypy in sabnzbd (#54539) --- homeassistant/components/sabnzbd/__init__.py | 4 +++- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index 8574e82aa47..a420ca53814 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -1,4 +1,6 @@ """Support for monitoring an SABnzbd NZB client.""" +from __future__ import annotations + from datetime import timedelta import logging @@ -31,7 +33,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "sabnzbd" DATA_SABNZBD = "sabznbd" -_CONFIGURING = {} +_CONFIGURING: dict[str, str] = {} ATTR_SPEED = "speed" BASE_URL_FORMAT = "{}://{}:{}/" diff --git a/mypy.ini b/mypy.ini index 91b40b63cc1..0f025345638 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1598,9 +1598,6 @@ ignore_errors = true [mypy-homeassistant.components.ruckus_unleashed.*] ignore_errors = true -[mypy-homeassistant.components.sabnzbd.*] -ignore_errors = true - [mypy-homeassistant.components.screenlogic.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index a69a9ec8d88..508c1fcb26a 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -125,7 +125,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.ring.*", "homeassistant.components.rpi_power.*", "homeassistant.components.ruckus_unleashed.*", - "homeassistant.components.sabnzbd.*", "homeassistant.components.screenlogic.*", "homeassistant.components.search.*", "homeassistant.components.sense.*", From 848c0be58a45371cb2b9a03398075ef00d136331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 17 Aug 2021 00:12:06 +0300 Subject: [PATCH 249/355] Avoid some implicit generic Anys (#54577) --- homeassistant/auth/auth_store.py | 4 +++- homeassistant/auth/providers/__init__.py | 4 +++- homeassistant/auth/providers/command_line.py | 4 +++- homeassistant/auth/providers/homeassistant.py | 4 +++- .../auth/providers/insecure_example.py | 6 ++++-- .../auth/providers/legacy_api_password.py | 6 ++++-- .../auth/providers/trusted_networks.py | 4 +++- homeassistant/config_entries.py | 14 +++++++++++--- homeassistant/exceptions.py | 10 ++++++---- homeassistant/helpers/reload.py | 17 +++++++++++------ homeassistant/helpers/script_variables.py | 4 +++- homeassistant/scripts/__init__.py | 4 ++-- homeassistant/setup.py | 11 ++++++++--- homeassistant/util/color.py | 6 +++++- 14 files changed, 69 insertions(+), 29 deletions(-) diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 0b360668ad4..63cbeb1bf7e 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -17,6 +17,8 @@ from .const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY, GROUP_ID_USER from .permissions import PermissionLookup, system_policies from .permissions.types import PolicyType +# mypy: disallow-any-generics + STORAGE_VERSION = 1 STORAGE_KEY = "auth" GROUP_NAME_ADMIN = "Administrators" @@ -491,7 +493,7 @@ class AuthStore: self._store.async_delay_save(self._data_to_save, 1) @callback - def _data_to_save(self) -> dict: + def _data_to_save(self) -> dict[str, list[dict[str, Any]]]: """Return the data to store.""" assert self._users is not None assert self._groups is not None diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index d2dfa0e1c6d..4faa277a081 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -22,6 +22,8 @@ from ..auth_store import AuthStore from ..const import MFA_SESSION_EXPIRATION from ..models import Credentials, RefreshToken, User, UserMeta +# mypy: disallow-any-generics + _LOGGER = logging.getLogger(__name__) DATA_REQS = "auth_prov_reqs_processed" @@ -96,7 +98,7 @@ class AuthProvider: # Implement by extending class - async def async_login_flow(self, context: dict | None) -> LoginFlow: + async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow: """Return the data flow for logging in with auth provider. Auth provider should extend LoginFlow and return an instance. diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py index f462ad4be9d..6d1a1627fd5 100644 --- a/homeassistant/auth/providers/command_line.py +++ b/homeassistant/auth/providers/command_line.py @@ -17,6 +17,8 @@ from homeassistant.exceptions import HomeAssistantError from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from ..models import Credentials, UserMeta +# mypy: disallow-any-generics + CONF_ARGS = "args" CONF_META = "meta" @@ -56,7 +58,7 @@ class CommandLineAuthProvider(AuthProvider): super().__init__(*args, **kwargs) self._user_meta: dict[str, dict[str, Any]] = {} - async def async_login_flow(self, context: dict | None) -> LoginFlow: + async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow: """Return a flow to login.""" return CommandLineLoginFlow(self) diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index dfbf077a89d..b08c59bf3aa 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -19,6 +19,8 @@ from homeassistant.exceptions import HomeAssistantError from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from ..models import Credentials, UserMeta +# mypy: disallow-any-generics + STORAGE_VERSION = 1 STORAGE_KEY = "auth_provider.homeassistant" @@ -235,7 +237,7 @@ class HassAuthProvider(AuthProvider): await data.async_load() self.data = data - async def async_login_flow(self, context: dict | None) -> LoginFlow: + async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow: """Return a flow to login.""" return HassLoginFlow(self) diff --git a/homeassistant/auth/providers/insecure_example.py b/homeassistant/auth/providers/insecure_example.py index 5a3a890ff66..fb390b65b0d 100644 --- a/homeassistant/auth/providers/insecure_example.py +++ b/homeassistant/auth/providers/insecure_example.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections import OrderedDict from collections.abc import Mapping import hmac -from typing import cast +from typing import Any, cast import voluptuous as vol @@ -15,6 +15,8 @@ from homeassistant.exceptions import HomeAssistantError from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from ..models import Credentials, UserMeta +# mypy: disallow-any-generics + USER_SCHEMA = vol.Schema( { vol.Required("username"): str, @@ -37,7 +39,7 @@ class InvalidAuthError(HomeAssistantError): class ExampleAuthProvider(AuthProvider): """Example auth provider based on hardcoded usernames and passwords.""" - async def async_login_flow(self, context: dict | None) -> LoginFlow: + async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow: """Return a flow to login.""" return ExampleLoginFlow(self) diff --git a/homeassistant/auth/providers/legacy_api_password.py b/homeassistant/auth/providers/legacy_api_password.py index b385aa0ed59..af24506210b 100644 --- a/homeassistant/auth/providers/legacy_api_password.py +++ b/homeassistant/auth/providers/legacy_api_password.py @@ -7,7 +7,7 @@ from __future__ import annotations from collections.abc import Mapping import hmac -from typing import cast +from typing import Any, cast import voluptuous as vol @@ -19,6 +19,8 @@ import homeassistant.helpers.config_validation as cv from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from ..models import Credentials, UserMeta +# mypy: disallow-any-generics + AUTH_PROVIDER_TYPE = "legacy_api_password" CONF_API_PASSWORD = "api_password" @@ -44,7 +46,7 @@ class LegacyApiPasswordAuthProvider(AuthProvider): """Return api_password.""" return str(self.config[CONF_API_PASSWORD]) - async def async_login_flow(self, context: dict | None) -> LoginFlow: + async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow: """Return a flow to login.""" return LegacyLoginFlow(self) diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index 7b609f371ef..a9ee6a48335 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -27,6 +27,8 @@ from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from .. import InvalidAuthError from ..models import Credentials, RefreshToken, UserMeta +# mypy: disallow-any-generics + IPAddress = Union[IPv4Address, IPv6Address] IPNetwork = Union[IPv4Network, IPv6Network] @@ -97,7 +99,7 @@ class TrustedNetworksAuthProvider(AuthProvider): """Trusted Networks auth provider does not support MFA.""" return False - async def async_login_flow(self, context: dict | None) -> LoginFlow: + async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow: """Return a flow to login.""" assert context is not None ip_addr = cast(IPAddress, context.get("ip_address")) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index ec074f81b95..07cb9eae7f9 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -21,7 +21,12 @@ from homeassistant.exceptions import ( ) from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.event import Event -from homeassistant.helpers.typing import UNDEFINED, DiscoveryInfoType, UndefinedType +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 @@ -598,7 +603,10 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): """Manage all the config entry flows that are in progress.""" def __init__( - self, hass: HomeAssistant, config_entries: ConfigEntries, hass_config: dict + self, + hass: HomeAssistant, + config_entries: ConfigEntries, + hass_config: ConfigType, ) -> None: """Initialize the config entry flow manager.""" super().__init__(hass) @@ -748,7 +756,7 @@ class ConfigEntries: An instance of this object is available via `hass.config_entries`. """ - def __init__(self, hass: HomeAssistant, hass_config: dict) -> None: + def __init__(self, hass: HomeAssistant, hass_config: ConfigType) -> None: """Initialize the entry manager.""" self.hass = hass self.flow = ConfigEntriesFlowManager(hass, self, hass_config) diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 844fd369cac..2a82c2652ed 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -9,6 +9,8 @@ import attr if TYPE_CHECKING: from .core import Context +# mypy: disallow-any-generics + class HomeAssistantError(Exception): """General Home Assistant exception occurred.""" @@ -42,7 +44,7 @@ class ConditionError(HomeAssistantError): """Return indentation.""" return " " * indent + message - def output(self, indent: int) -> Generator: + def output(self, indent: int) -> Generator[str, None, None]: """Yield an indented representation.""" raise NotImplementedError() @@ -58,7 +60,7 @@ class ConditionErrorMessage(ConditionError): # A message describing this error message: str = attr.ib() - def output(self, indent: int) -> Generator: + def output(self, indent: int) -> Generator[str, None, None]: """Yield an indented representation.""" yield self._indent(indent, f"In '{self.type}' condition: {self.message}") @@ -74,7 +76,7 @@ class ConditionErrorIndex(ConditionError): # The error that this error wraps error: ConditionError = attr.ib() - def output(self, indent: int) -> Generator: + def output(self, indent: int) -> Generator[str, None, None]: """Yield an indented representation.""" if self.total > 1: yield self._indent( @@ -93,7 +95,7 @@ class ConditionErrorContainer(ConditionError): # List of ConditionErrors that this error wraps errors: Sequence[ConditionError] = attr.ib() - def output(self, indent: int) -> Generator: + def output(self, indent: int) -> Generator[str, None, None]: """Yield an indented representation.""" for item in self.errors: yield from item.output(indent) diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index da6c6935b35..cedd07676ba 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Iterable import logging +from typing import Any from homeassistant import config as conf_util from homeassistant.const import SERVICE_RELOAD @@ -15,11 +16,13 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component +# mypy: disallow-any-generics + _LOGGER = logging.getLogger(__name__) async def async_reload_integration_platforms( - hass: HomeAssistant, integration_name: str, integration_platforms: Iterable + hass: HomeAssistant, integration_name: str, integration_platforms: Iterable[str] ) -> None: """Reload an integration's platforms. @@ -62,7 +65,7 @@ async def _resetup_platform( if not conf: return - root_config: dict = {integration_platform: []} + root_config: dict[str, Any] = {integration_platform: []} # Extract only the config for template, ignore the rest. for p_type, p_config in config_per_platform(conf, integration_platform): if p_type != integration_name: @@ -102,7 +105,7 @@ async def _async_setup_platform( hass: HomeAssistant, integration_name: str, integration_platform: str, - platform_configs: list[dict], + platform_configs: list[dict[str, Any]], ) -> None: """Platform for the first time when new configuration is added.""" if integration_platform not in hass.data: @@ -120,7 +123,7 @@ async def _async_setup_platform( async def _async_reconfig_platform( - platform: EntityPlatform, platform_configs: list[dict] + platform: EntityPlatform, platform_configs: list[dict[str, Any]] ) -> None: """Reconfigure an already loaded platform.""" await platform.async_reset() @@ -155,7 +158,7 @@ def async_get_platform_without_config_entry( async def async_setup_reload_service( - hass: HomeAssistant, domain: str, platforms: Iterable + hass: HomeAssistant, domain: str, platforms: Iterable[str] ) -> None: """Create the reload service for the domain.""" if hass.services.has_service(domain, SERVICE_RELOAD): @@ -171,7 +174,9 @@ async def async_setup_reload_service( ) -def setup_reload_service(hass: HomeAssistant, domain: str, platforms: Iterable) -> None: +def setup_reload_service( + hass: HomeAssistant, domain: str, platforms: Iterable[str] +) -> None: """Sync version of async_setup_reload_service.""" asyncio.run_coroutine_threadsafe( async_setup_reload_service(hass, domain, platforms), diff --git a/homeassistant/helpers/script_variables.py b/homeassistant/helpers/script_variables.py index 23241f22d1e..3dae84166f6 100644 --- a/homeassistant/helpers/script_variables.py +++ b/homeassistant/helpers/script_variables.py @@ -8,6 +8,8 @@ from homeassistant.core import HomeAssistant, callback from . import template +# mypy: disallow-any-generics + class ScriptVariables: """Class to hold and render script variables.""" @@ -65,6 +67,6 @@ class ScriptVariables: return rendered_variables - def as_dict(self) -> dict: + def as_dict(self) -> dict[str, Any]: """Return dict version of this class.""" return self.variables diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index b31fc718173..69ca1d6083b 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -15,10 +15,10 @@ from homeassistant.config import get_default_config_dir from homeassistant.requirements import pip_kwargs from homeassistant.util.package import install_package, is_installed, is_virtual_env -# mypy: allow-untyped-defs, no-warn-return-any +# mypy: allow-untyped-defs, disallow-any-generics, no-warn-return-any -def run(args: list) -> int: +def run(args: list[str]) -> int: """Run a script.""" scripts = [] path = os.path.dirname(__file__) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 9575a4331b8..95bb29c4b9d 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -16,10 +16,13 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, PLATFORM_FORMAT, ) +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 +# mypy: disallow-any-generics + _LOGGER = logging.getLogger(__name__) ATTR_COMPONENT = "component" @@ -422,7 +425,7 @@ def _async_when_setup( hass.async_create_task(when_setup()) return - listeners: list[Callable] = [] + listeners: list[CALLBACK_TYPE] = [] async def _matched_event(event: core.Event) -> None: """Call the callback when we matched an event.""" @@ -443,7 +446,7 @@ def _async_when_setup( @core.callback -def async_get_loaded_integrations(hass: core.HomeAssistant) -> set: +def async_get_loaded_integrations(hass: core.HomeAssistant) -> set[str]: """Return the complete list of loaded integrations.""" integrations = set() for component in hass.config.components: @@ -457,7 +460,9 @@ def async_get_loaded_integrations(hass: core.HomeAssistant) -> set: @contextlib.contextmanager -def async_start_setup(hass: core.HomeAssistant, components: Iterable) -> Generator: +def async_start_setup( + hass: core.HomeAssistant, components: Iterable[str] +) -> Generator[None, None, None]: """Keep track of when setup starts and finishes.""" setup_started = hass.data.setdefault(DATA_SETUP_STARTED, {}) started = dt_util.utcnow() diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 47144f0e782..c81beddb07a 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -6,6 +6,8 @@ import math import attr +# mypy: disallow-any-generics + # Official CSS3 colors from w3.org: # https://www.w3.org/TR/2010/PR-css3-color-20101028/#html4 # names do not have spaces in them so that we can compare against @@ -392,7 +394,9 @@ def color_hs_to_xy( return color_RGB_to_xy(*color_hs_to_RGB(iH, iS), Gamut) -def _match_max_scale(input_colors: tuple, output_colors: tuple) -> tuple: +def _match_max_scale( + input_colors: tuple[int, ...], output_colors: tuple[int, ...] +) -> tuple[int, ...]: """Match the maximum value of the output to the input.""" max_in = max(input_colors) max_out = max(output_colors) From 85ff5e34cd4a06be8dfa96b5d9959be91f0161d3 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 16 Aug 2021 23:25:41 +0200 Subject: [PATCH 250/355] Active mypy for netio (#54543) --- homeassistant/components/netio/switch.py | 7 +++++-- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/netio/switch.py b/homeassistant/components/netio/switch.py index c39b1598c89..88da77cbf90 100644 --- a/homeassistant/components/netio/switch.py +++ b/homeassistant/components/netio/switch.py @@ -1,7 +1,10 @@ """The Netio switch component.""" +from __future__ import annotations + from collections import namedtuple from datetime import timedelta import logging +from typing import Any from pynetio import Netio import voluptuous as vol @@ -29,8 +32,8 @@ CONF_OUTLETS = "outlets" DEFAULT_PORT = 1234 DEFAULT_USERNAME = "admin" -Device = namedtuple("device", ["netio", "entities"]) -DEVICES = {} +Device = namedtuple("Device", ["netio", "entities"]) +DEVICES: dict[str, Any] = {} MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) diff --git a/mypy.ini b/mypy.ini index 0f025345638..a12719f90df 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1514,9 +1514,6 @@ ignore_errors = true [mypy-homeassistant.components.nest.legacy.*] ignore_errors = true -[mypy-homeassistant.components.netio.*] -ignore_errors = true - [mypy-homeassistant.components.nightscout.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 508c1fcb26a..dc00be3efe4 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -97,7 +97,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.mullvad.*", "homeassistant.components.ness_alarm.*", "homeassistant.components.nest.legacy.*", - "homeassistant.components.netio.*", "homeassistant.components.nightscout.*", "homeassistant.components.nilu.*", "homeassistant.components.nmap_tracker.*", From de0460de6170b360fe6746e5f2963a04e6063547 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 16 Aug 2021 22:33:28 +0100 Subject: [PATCH 251/355] Add device classes that were part of deprecated air quality entity (#54075) --- homeassistant/components/sensor/__init__.py | 18 +++++++++++ .../components/sensor/device_condition.py | 32 +++++++++++++++++++ .../components/sensor/device_trigger.py | 32 +++++++++++++++++++ homeassistant/components/sensor/strings.json | 16 ++++++++++ homeassistant/const.py | 9 ++++++ .../components/sensor/test_device_trigger.py | 2 +- .../custom_components/test/sensor.py | 9 ++++++ 7 files changed, 117 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 087328ed4a6..950af5a1375 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -12,6 +12,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + DEVICE_CLASS_AQI, DEVICE_CLASS_BATTERY, DEVICE_CLASS_CO, DEVICE_CLASS_CO2, @@ -21,10 +22,18 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_MONETARY, + 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_TIMESTAMP, DEVICE_CLASS_VOLTAGE, @@ -51,6 +60,7 @@ ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" SCAN_INTERVAL: Final = timedelta(seconds=30) DEVICE_CLASSES: Final[list[str]] = [ + DEVICE_CLASS_AQI, # Air Quality Index DEVICE_CLASS_BATTERY, # % of battery that is left DEVICE_CLASS_CO, # ppm (parts per million) Carbon Monoxide gas concentration DEVICE_CLASS_CO2, # ppm (parts per million) Carbon Dioxide gas concentration @@ -59,7 +69,15 @@ DEVICE_CLASSES: Final[list[str]] = [ DEVICE_CLASS_HUMIDITY, # % of humidity in the air DEVICE_CLASS_ILLUMINANCE, # current light level (lx/lm) DEVICE_CLASS_MONETARY, # Amount of money (currency) + DEVICE_CLASS_OZONE, # Amount of O3 (µg/m³) + DEVICE_CLASS_NITROGEN_DIOXIDE, # Amount of NO2 (µg/m³) + DEVICE_CLASS_NITROUS_OXIDE, # Amount of NO (µg/m³) + DEVICE_CLASS_NITROGEN_MONOXIDE, # Amount of N2O (µg/m³) + DEVICE_CLASS_PM1, # Particulate matter <= 0.1 μm (µg/m³) + DEVICE_CLASS_PM10, # Particulate matter <= 10 μm (µg/m³) + DEVICE_CLASS_PM25, # Particulate matter <= 2.5 μm (µg/m³) DEVICE_CLASS_SIGNAL_STRENGTH, # signal strength (dB/dBm) + DEVICE_CLASS_SULPHUR_DIOXIDE, # Amount of SO2 (µg/m³) DEVICE_CLASS_TEMPERATURE, # temperature (C/F) DEVICE_CLASS_TIMESTAMP, # timestamp (ISO8601) DEVICE_CLASS_PRESSURE, # pressure (hPa/mbar) diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 3b9f3839cfb..dee20405e07 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -19,10 +19,18 @@ from homeassistant.const import ( 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_VOLTAGE, ) @@ -49,10 +57,18 @@ CONF_IS_ENERGY = "is_energy" CONF_IS_HUMIDITY = "is_humidity" CONF_IS_GAS = "is_gas" CONF_IS_ILLUMINANCE = "is_illuminance" +CONF_IS_NITROGEN_DIOXIDE = "is_nitrogen_dioxide" +CONF_IS_NITROGEN_MONOXIDE = "is_nitrogen_monoxide" +CONF_IS_NITROUS_OXIDE = "is_nitrous_oxide" +CONF_IS_OZONE = "is_ozone" +CONF_IS_PM1 = "is_pm1" +CONF_IS_PM10 = "is_pm10" +CONF_IS_PM25 = "is_pm25" CONF_IS_POWER = "is_power" CONF_IS_POWER_FACTOR = "is_power_factor" CONF_IS_PRESSURE = "is_pressure" CONF_IS_SIGNAL_STRENGTH = "is_signal_strength" +CONF_IS_SULPHUR_DIOXIDE = "is_sulphur_dioxide" CONF_IS_TEMPERATURE = "is_temperature" CONF_IS_VOLTAGE = "is_voltage" CONF_IS_VALUE = "is_value" @@ -66,10 +82,18 @@ ENTITY_CONDITIONS = { 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_VOLTAGE: [{CONF_TYPE: CONF_IS_VOLTAGE}], DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_VALUE}], @@ -89,10 +113,18 @@ CONDITION_SCHEMA = vol.All( CONF_IS_GAS, CONF_IS_HUMIDITY, CONF_IS_ILLUMINANCE, + CONF_IS_OZONE, + CONF_IS_NITROGEN_DIOXIDE, + CONF_IS_NITROGEN_MONOXIDE, + CONF_IS_NITROUS_OXIDE, CONF_IS_POWER, CONF_IS_POWER_FACTOR, + CONF_IS_PM1, + CONF_IS_PM10, + CONF_IS_PM25, CONF_IS_PRESSURE, CONF_IS_SIGNAL_STRENGTH, + CONF_IS_SULPHUR_DIOXIDE, CONF_IS_TEMPERATURE, CONF_IS_VOLTAGE, CONF_IS_VALUE, diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index f7d72dd4c1b..2de09c01bc1 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -22,10 +22,18 @@ from homeassistant.const import ( 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_VOLTAGE, ) @@ -48,10 +56,18 @@ CONF_ENERGY = "energy" CONF_GAS = "gas" CONF_HUMIDITY = "humidity" CONF_ILLUMINANCE = "illuminance" +CONF_NITROGEN_DIOXIDE = "nitrogen_dioxide" +CONF_NITROGEN_MONOXIDE = "nitrogen_monoxide" +CONF_NITROUS_OXIDE = "nitrous_oxide" +CONF_OZONE = "ozone" +CONF_PM1 = "pm1" +CONF_PM10 = "pm10" +CONF_PM25 = "pm25" CONF_POWER = "power" CONF_POWER_FACTOR = "power_factor" CONF_PRESSURE = "pressure" CONF_SIGNAL_STRENGTH = "signal_strength" +CONF_SULPHUR_DIOXIDE = "sulphur_dioxide" CONF_TEMPERATURE = "temperature" CONF_VOLTAGE = "voltage" CONF_VALUE = "value" @@ -65,10 +81,18 @@ ENTITY_TRIGGERS = { 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_VOLTAGE: [{CONF_TYPE: CONF_VOLTAGE}], DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_VALUE}], @@ -89,10 +113,18 @@ TRIGGER_SCHEMA = vol.All( CONF_GAS, CONF_HUMIDITY, CONF_ILLUMINANCE, + CONF_NITROGEN_DIOXIDE, + CONF_NITROGEN_MONOXIDE, + CONF_NITROUS_OXIDE, + CONF_OZONE, + CONF_PM1, + CONF_PM10, + CONF_PM25, CONF_POWER, CONF_POWER_FACTOR, CONF_PRESSURE, CONF_SIGNAL_STRENGTH, + CONF_SULPHUR_DIOXIDE, CONF_TEMPERATURE, CONF_VOLTAGE, CONF_VALUE, diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index 54d0f9ad76c..431e8a4789a 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -8,9 +8,17 @@ "is_gas": "Current {entity_name} gas", "is_humidity": "Current {entity_name} humidity", "is_illuminance": "Current {entity_name} illuminance", + "is_nitrogen_dioxide": "Current {entity_name} nitrogen dioxide concentration level", + "is_nitrogen_monoxide": "Current {entity_name} nitrogen monoxide concentration level", + "is_nitrous_oxide": "Current {entity_name} nitrous oxide concentration level", + "is_ozone": "Current {entity_name} ozone concentration level", + "is_pm1": "Current {entity_name} PM1 concentration level", + "is_pm10": "Current {entity_name} PM10 concentration level", + "is_pm25": "Current {entity_name} PM2.5 concentration level", "is_power": "Current {entity_name} power", "is_pressure": "Current {entity_name} pressure", "is_signal_strength": "Current {entity_name} signal strength", + "is_sulphur_dioxide": "Current {entity_name} sulphur dioxide concentration level", "is_temperature": "Current {entity_name} temperature", "is_current": "Current {entity_name} current", "is_energy": "Current {entity_name} energy", @@ -25,9 +33,17 @@ "gas": "{entity_name} gas changes", "humidity": "{entity_name} humidity changes", "illuminance": "{entity_name} illuminance changes", + "nitrogen_dioxide": "{entity_name} nitrogen dioxide concentration changes", + "nitrogen_monoxide": "{entity_name} nitrogen monoxide concentration changes", + "nitrous_oxide": "{entity_name} nitrous oxide concentration changes", + "ozone": "{entity_name} ozone concentration changes", + "pm1": "{entity_name} PM1 concentration changes", + "pm10": "{entity_name} PM10 concentration changes", + "pm25": "{entity_name} PM2.5 concentration changes", "power": "{entity_name} power changes", "pressure": "{entity_name} pressure changes", "signal_strength": "{entity_name} signal strength changes", + "sulphur_dioxide": "{entity_name} sulphur dioxide concentration changes", "temperature": "{entity_name} temperature changes", "current": "{entity_name} current changes", "energy": "{entity_name} energy changes", diff --git a/homeassistant/const.py b/homeassistant/const.py index 9fa5c2cd231..ae1f50d0087 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -232,6 +232,7 @@ EVENT_TIME_CHANGED: Final = "time_changed" # #### DEVICE CLASSES #### +DEVICE_CLASS_AQI: Final = "aqi" DEVICE_CLASS_BATTERY: Final = "battery" DEVICE_CLASS_CO: Final = "carbon_monoxide" DEVICE_CLASS_CO2: Final = "carbon_dioxide" @@ -240,10 +241,18 @@ DEVICE_CLASS_ENERGY: Final = "energy" DEVICE_CLASS_HUMIDITY: Final = "humidity" DEVICE_CLASS_ILLUMINANCE: Final = "illuminance" DEVICE_CLASS_MONETARY: Final = "monetary" +DEVICE_CLASS_NITROGEN_DIOXIDE = "nitrogen_dioxide" +DEVICE_CLASS_NITROGEN_MONOXIDE = "nitrogen_monoxide" +DEVICE_CLASS_NITROUS_OXIDE = "nitrous_oxide" +DEVICE_CLASS_OZONE: Final = "ozone" DEVICE_CLASS_POWER_FACTOR: Final = "power_factor" DEVICE_CLASS_POWER: Final = "power" +DEVICE_CLASS_PM25: Final = "pm25" +DEVICE_CLASS_PM1: Final = "pm1" +DEVICE_CLASS_PM10: Final = "pm10" DEVICE_CLASS_PRESSURE: Final = "pressure" DEVICE_CLASS_SIGNAL_STRENGTH: Final = "signal_strength" +DEVICE_CLASS_SULPHUR_DIOXIDE = "sulphur_dioxide" DEVICE_CLASS_TEMPERATURE: Final = "temperature" DEVICE_CLASS_TIMESTAMP: Final = "timestamp" DEVICE_CLASS_VOLTAGE: Final = "voltage" diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index f955c3c19db..8e60714a9e2 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -86,7 +86,7 @@ async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrat if device_class != "none" ] triggers = await async_get_device_automations(hass, "trigger", device_entry.id) - assert len(triggers) == 14 + assert len(triggers) == 22 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 63f47a0f854..010b82dc3a2 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -5,6 +5,7 @@ Call init before using it in your tests to ensure clean test data. """ import homeassistant.components.sensor as sensor from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE, PRESSURE_HPA, @@ -23,7 +24,15 @@ UNITS_OF_MEASUREMENT = { 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) From 684d035969a28de6cfbf1b560fed80711966ad6b Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 16 Aug 2021 23:54:11 +0200 Subject: [PATCH 252/355] Use state class total increasing for TPLink smart plugs (#54723) --- homeassistant/components/tplink/__init__.py | 8 +------- homeassistant/components/tplink/sensor.py | 9 +++------ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 552e5666db8..5c69247eea8 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -1,7 +1,7 @@ """Component to embed TP-Link smart home devices.""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import timedelta import logging import time from typing import Any @@ -11,7 +11,6 @@ from pyHS100.smartplug import SmartPlug import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.sensor import ATTR_LAST_RESET from homeassistant.components.switch import ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -28,7 +27,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from homeassistant.util.dt import utc_from_timestamp from .common import SmartDevices, async_discover_devices, get_static_devices from .const import ( @@ -258,12 +256,8 @@ class SmartPlugDataUpdateCoordinator(DataUpdateCoordinator): ATTR_TOTAL_ENERGY_KWH: round(float(emeter_readings["total"]), 3), ATTR_VOLTAGE: round(float(emeter_readings["voltage"]), 1), ATTR_CURRENT_A: round(float(emeter_readings["current"]), 2), - ATTR_LAST_RESET: {ATTR_TOTAL_ENERGY_KWH: utc_from_timestamp(0)}, } emeter_statics = self.smartplug.get_emeter_daily() - data[CONF_EMETER_PARAMS][ATTR_LAST_RESET][ - ATTR_TODAY_ENERGY_KWH - ] = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) if emeter_statics.get(int(time.strftime("%e"))): data[CONF_EMETER_PARAMS][ATTR_TODAY_ENERGY_KWH] = round( float(emeter_statics[int(time.strftime("%e"))]), 3 diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index fae7939cd65..b38fa763ee9 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -6,8 +6,8 @@ from typing import Any, Final from pyHS100 import SmartPlug from homeassistant.components.sensor import ( - ATTR_LAST_RESET, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, ) @@ -62,14 +62,14 @@ ENERGY_SENSORS: Final[list[SensorEntityDescription]] = [ key=ATTR_TOTAL_ENERGY_KWH, unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, name="Total Consumption", ), SensorEntityDescription( key=ATTR_TODAY_ENERGY_KWH, unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, name="Today's Consumption", ), SensorEntityDescription( @@ -127,9 +127,6 @@ class SmartPlugSensor(CoordinatorEntity, SensorEntity): self.smartplug = smartplug self.entity_description = description self._attr_name = f"{coordinator.data[CONF_ALIAS]} {description.name}" - self._attr_last_reset = coordinator.data[CONF_EMETER_PARAMS][ - ATTR_LAST_RESET - ].get(description.key) @property def data(self) -> dict[str, Any]: From 41c3bd113c6ab9a6865863bdff7593466bec775d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Aug 2021 16:54:45 -0500 Subject: [PATCH 253/355] Bump zeroconf to 0.36.0 (#54720) --- 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 05576accb78..84f9f4698e9 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.35.1"], + "requirements": ["zeroconf==0.36.0"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9409432e13f..3729b393470 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ sqlalchemy==1.4.17 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 -zeroconf==0.35.1 +zeroconf==0.36.0 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 2a03e1c4855..dfbfd346929 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2443,7 +2443,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.35.1 +zeroconf==0.36.0 # homeassistant.components.zha zha-quirks==0.0.59 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 706dcf1da77..8ab3042137d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1354,7 +1354,7 @@ youless-api==0.10 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.35.1 +zeroconf==0.36.0 # homeassistant.components.zha zha-quirks==0.0.59 From 0abcfb42b3dd39a51c02ae9755f239ccfd84acce Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 16 Aug 2021 23:57:59 +0200 Subject: [PATCH 254/355] Remove last_reset attribute from FritzBoxEnergySensor (#54644) --- homeassistant/components/fritzbox/sensor.py | 12 ++---------- tests/components/fritzbox/test_switch.py | 5 ++--- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 56e025cd605..09a652d64ad 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -1,13 +1,12 @@ """Support for AVM FRITZ!SmartHome temperature sensor only devices.""" from __future__ import annotations -from datetime import datetime - from pyfritzhome import FritzhomeDevice from homeassistant.components.sensor import ( ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -28,7 +27,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from homeassistant.util.dt import utc_from_timestamp from . import FritzBoxEntity from .const import ( @@ -99,7 +97,7 @@ async def async_setup_entry( ATTR_ENTITY_ID: f"{device.ain}_total_energy", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, coordinator, ain, @@ -153,12 +151,6 @@ class FritzBoxEnergySensor(FritzBoxSensor): return energy / 1000 # type: ignore [no-any-return] return 0.0 - @property - def last_reset(self) -> datetime: - """Return the time when the sensor was last reset, if any.""" - # device does not provide timestamp of initialization - return utc_from_timestamp(0) - class FritzBoxTempSensor(FritzBoxSensor): """The entity class for FRITZ!SmartHome temperature sensors.""" diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index 951528f1e7d..27461b2790f 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -10,10 +10,10 @@ from homeassistant.components.fritzbox.const import ( DOMAIN as FB_DOMAIN, ) from homeassistant.components.sensor import ( - ATTR_LAST_RESET, ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, ) from homeassistant.components.switch import DOMAIN from homeassistant.const import ( @@ -73,10 +73,9 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_total_energy") assert state assert state.state == "1.234" - assert state.attributes[ATTR_LAST_RESET] == "1970-01-01T00:00:00+00:00" 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_MEASUREMENT + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING async def test_turn_on(hass: HomeAssistant, fritz: Mock): From 38a210292f40dab36dfcf8be950f0231d50dde08 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 17 Aug 2021 00:14:00 +0200 Subject: [PATCH 255/355] Use EntityDescription - logi_circle (#54429) --- .../components/logi_circle/__init__.py | 8 +- homeassistant/components/logi_circle/const.py | 46 +++++++-- .../components/logi_circle/sensor.py | 95 ++++++++----------- 3 files changed, 81 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index 9e1a4803e11..d9060b10080 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -29,8 +29,8 @@ from .const import ( DEFAULT_CACHEDB, DOMAIN, LED_MODE_KEY, - LOGI_SENSORS, RECORDING_MODE_KEY, + SENSOR_TYPES, SIGNAL_LOGI_CIRCLE_RECONFIGURE, SIGNAL_LOGI_CIRCLE_RECORD, SIGNAL_LOGI_CIRCLE_SNAPSHOT, @@ -50,10 +50,12 @@ ATTR_DURATION = "duration" PLATFORMS = ["camera", "sensor"] +SENSOR_KEYS = [desc.key for desc in SENSOR_TYPES] + SENSOR_SCHEMA = vol.Schema( { - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(LOGI_SENSORS)): vol.All( - cv.ensure_list, [vol.In(LOGI_SENSORS)] + vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( + cv.ensure_list, [vol.In(SENSOR_KEYS)] ) } ) diff --git a/homeassistant/components/logi_circle/const.py b/homeassistant/components/logi_circle/const.py index 92967d2eb84..02e51993198 100644 --- a/homeassistant/components/logi_circle/const.py +++ b/homeassistant/components/logi_circle/const.py @@ -1,4 +1,7 @@ """Constants in Logi Circle component.""" +from __future__ import annotations + +from homeassistant.components.sensor import SensorEntityDescription from homeassistant.const import PERCENTAGE DOMAIN = "logi_circle" @@ -12,15 +15,40 @@ DEFAULT_CACHEDB = ".logi_cache.pickle" LED_MODE_KEY = "LED" RECORDING_MODE_KEY = "RECORDING_MODE" -# Sensor types: Name, unit of measure, icon per sensor key. -LOGI_SENSORS = { - "battery_level": ["Battery", PERCENTAGE, "battery-50"], - "last_activity_time": ["Last Activity", None, "history"], - "recording": ["Recording Mode", None, "eye"], - "signal_strength_category": ["WiFi Signal Category", None, "wifi"], - "signal_strength_percentage": ["WiFi Signal Strength", PERCENTAGE, "wifi"], - "streaming": ["Streaming Mode", None, "camera"], -} +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="battery_level", + name="Battery", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:battery-50", + ), + SensorEntityDescription( + key="last_activity_time", + name="Last Activity", + icon="mdi:history", + ), + SensorEntityDescription( + key="recording", + name="Recording Mode", + icon="mdi:eye", + ), + SensorEntityDescription( + key="signal_strength_category", + name="WiFi Signal Category", + icon="mdi:wifi", + ), + SensorEntityDescription( + key="signal_strength_percentage", + name="WiFi Signal Strength", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:wifi", + ), + SensorEntityDescription( + key="streaming", + name="Streaming Mode", + icon="mdi:camera", + ), +) SIGNAL_LOGI_CIRCLE_RECONFIGURE = "logi_circle_reconfigure" SIGNAL_LOGI_CIRCLE_SNAPSHOT = "logi_circle_snapshot" diff --git a/homeassistant/components/logi_circle/sensor.py b/homeassistant/components/logi_circle/sensor.py index a4158762b37..50671152587 100644 --- a/homeassistant/components/logi_circle/sensor.py +++ b/homeassistant/components/logi_circle/sensor.py @@ -1,7 +1,10 @@ """Support for Logi Circle sensors.""" -import logging +from __future__ import annotations -from homeassistant.components.sensor import SensorEntity +import logging +from typing import Any + +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_BATTERY_CHARGING, @@ -13,12 +16,7 @@ from homeassistant.const import ( from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util.dt import as_local -from .const import ( - ATTRIBUTION, - DEVICE_BRAND, - DOMAIN as LOGI_CIRCLE_DOMAIN, - LOGI_SENSORS as SENSOR_TYPES, -) +from .const import ATTRIBUTION, DEVICE_BRAND, DOMAIN as LOGI_CIRCLE_DOMAIN, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) @@ -33,44 +31,30 @@ async def async_setup_entry(hass, entry, async_add_entities): devices = await hass.data[LOGI_CIRCLE_DOMAIN].cameras time_zone = str(hass.config.time_zone) - sensors = [] - for sensor_type in entry.data.get(CONF_SENSORS).get(CONF_MONITORED_CONDITIONS): - for device in devices: - if device.supports_feature(sensor_type): - sensors.append(LogiSensor(device, time_zone, sensor_type)) + monitored_conditions = entry.data.get(CONF_SENSORS).get(CONF_MONITORED_CONDITIONS) + entities = [ + LogiSensor(device, time_zone, description) + for description in SENSOR_TYPES + if description.key in monitored_conditions + for device in devices + if device.supports_feature(description.key) + ] - async_add_entities(sensors, True) + async_add_entities(entities, True) class LogiSensor(SensorEntity): """A sensor implementation for a Logi Circle camera.""" - def __init__(self, camera, time_zone, sensor_type): + def __init__(self, camera, time_zone, description: SensorEntityDescription): """Initialize a sensor for Logi Circle camera.""" - self._sensor_type = sensor_type + self.entity_description = description self._camera = camera - self._id = f"{self._camera.mac_address}-{self._sensor_type}" - self._icon = f"mdi:{SENSOR_TYPES.get(self._sensor_type)[2]}" - self._name = f"{self._camera.name} {SENSOR_TYPES.get(self._sensor_type)[0]}" - self._activity = {} - self._state = None + self._attr_unique_id = f"{camera.mac_address}-{description.key}" + self._attr_name = f"{camera.name} {description.name}" + self._activity: dict[Any, Any] = {} self._tz = time_zone - @property - def unique_id(self): - """Return a unique ID.""" - return self._id - - @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_info(self): """Return information about the device.""" @@ -93,7 +77,7 @@ class LogiSensor(SensorEntity): "microphone_gain": self._camera.microphone_gain, } - if self._sensor_type == "battery_level": + if self.entity_description.key == "battery_level": state[ATTR_BATTERY_CHARGING] = self._camera.charging return state @@ -101,37 +85,36 @@ class LogiSensor(SensorEntity): @property def icon(self): """Icon to use in the frontend, if any.""" - if self._sensor_type == "battery_level" and self._state is not None: + sensor_type = self.entity_description.key + if sensor_type == "battery_level" and self._attr_native_value is not None: return icon_for_battery_level( - battery_level=int(self._state), charging=False + battery_level=int(self._attr_native_value), charging=False ) - if self._sensor_type == "recording_mode" and self._state is not None: - return "mdi:eye" if self._state == STATE_ON else "mdi:eye-off" - if self._sensor_type == "streaming_mode" and self._state is not None: - return "mdi:camera" if self._state == STATE_ON else "mdi:camera-off" - return self._icon - - @property - def native_unit_of_measurement(self): - """Return the units of measurement.""" - return SENSOR_TYPES.get(self._sensor_type)[1] + if sensor_type == "recording_mode" and self._attr_native_value is not None: + return "mdi:eye" if self._attr_native_value == STATE_ON else "mdi:eye-off" + if sensor_type == "streaming_mode" and self._attr_native_value is not None: + return ( + "mdi:camera" + if self._attr_native_value == STATE_ON + else "mdi:camera-off" + ) + return self.entity_description.icon async def async_update(self): """Get the latest data and updates the state.""" - _LOGGER.debug("Pulling data from %s sensor", self._name) + _LOGGER.debug("Pulling data from %s sensor", self.name) await self._camera.update() - if self._sensor_type == "last_activity_time": + if self.entity_description.key == "last_activity_time": last_activity = await self._camera.get_last_activity(force_refresh=True) if last_activity is not None: last_activity_time = as_local(last_activity.end_time_utc) - self._state = ( + self._attr_native_value = ( f"{last_activity_time.hour:0>2}:{last_activity_time.minute:0>2}" ) else: - state = getattr(self._camera, self._sensor_type, None) + state = getattr(self._camera, self.entity_description.key, None) if isinstance(state, bool): - self._state = STATE_ON if state is True else STATE_OFF + self._attr_native_value = STATE_ON if state is True else STATE_OFF else: - self._state = state - self._state = state + self._attr_native_value = state From 7524acc38c819bef7a83c0ce892fa621224b2cf3 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 17 Aug 2021 00:19:12 +0200 Subject: [PATCH 256/355] Activate mypy for sesame (#54546) --- homeassistant/components/sesame/lock.py | 29 +++++++------------------ mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 8 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/sesame/lock.py b/homeassistant/components/sesame/lock.py index acd71b7c9e7..261b2680499 100644 --- a/homeassistant/components/sesame/lock.py +++ b/homeassistant/components/sesame/lock.py @@ -1,17 +1,11 @@ """Support for Sesame, by CANDY HOUSE.""" -from typing import Callable +from __future__ import annotations import pysesame2 import voluptuous as vol from homeassistant.components.lock import PLATFORM_SCHEMA, LockEntity -from homeassistant.const import ( - ATTR_BATTERY_LEVEL, - ATTR_DEVICE_ID, - CONF_API_KEY, - STATE_LOCKED, - STATE_UNLOCKED, -) +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_DEVICE_ID, CONF_API_KEY import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -20,9 +14,7 @@ ATTR_SERIAL_NO = "serial" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string}) -def setup_platform( - hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None -): +def setup_platform(hass, config: ConfigType, add_entities, discovery_info=None): """Set up the Sesame platform.""" api_key = config.get(CONF_API_KEY) @@ -35,20 +27,20 @@ def setup_platform( class SesameDevice(LockEntity): """Representation of a Sesame device.""" - def __init__(self, sesame: object) -> None: + def __init__(self, sesame: pysesame2.Sesame) -> None: """Initialize the Sesame device.""" - self._sesame = sesame + self._sesame: pysesame2.Sesame = sesame # Cached properties from pysesame object. - self._device_id = None + self._device_id: str | None = None self._serial = None - self._nickname = None + self._nickname: str | None = None self._is_locked = False self._responsive = False self._battery = -1 @property - def name(self) -> str: + def name(self) -> str | None: """Return the name of the device.""" return self._nickname @@ -62,11 +54,6 @@ class SesameDevice(LockEntity): """Return True if the device is currently locked, else False.""" return self._is_locked - @property - def state(self) -> str: - """Get the state of the device.""" - return STATE_LOCKED if self._is_locked else STATE_UNLOCKED - def lock(self, **kwargs) -> None: """Lock the device.""" self._sesame.lock() diff --git a/mypy.ini b/mypy.ini index a12719f90df..837dc73343a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1604,9 +1604,6 @@ ignore_errors = true [mypy-homeassistant.components.sense.*] ignore_errors = true -[mypy-homeassistant.components.sesame.*] -ignore_errors = true - [mypy-homeassistant.components.sharkiq.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index dc00be3efe4..513be3e59c2 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -127,7 +127,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.screenlogic.*", "homeassistant.components.search.*", "homeassistant.components.sense.*", - "homeassistant.components.sesame.*", "homeassistant.components.sharkiq.*", "homeassistant.components.sma.*", "homeassistant.components.smartthings.*", From a6b1dbefd4a3296d7cabee3c616f49c1e948c7fc Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 17 Aug 2021 00:21:06 +0200 Subject: [PATCH 257/355] Use EntityDescription - mitemp_bt (#54503) --- homeassistant/components/mitemp_bt/sensor.py | 124 +++++++++---------- 1 file changed, 57 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py index 732beb11b3a..ed6c7f27b94 100644 --- a/homeassistant/components/mitemp_bt/sensor.py +++ b/homeassistant/components/mitemp_bt/sensor.py @@ -1,12 +1,19 @@ """Support for Xiaomi Mi Temp BLE environmental sensor.""" +from __future__ import annotations + import logging +from typing import Any import btlewrap from btlewrap.base import BluetoothBackendException from mitemp_bt import mitemp_bt_poller import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_MAC, @@ -44,18 +51,34 @@ DEFAULT_RETRIES = 2 DEFAULT_TIMEOUT = 10 -# Sensor types are defined like: Name, units -SENSOR_TYPES = { - "temperature": [DEVICE_CLASS_TEMPERATURE, "Temperature", TEMP_CELSIUS], - "humidity": [DEVICE_CLASS_HUMIDITY, "Humidity", PERCENTAGE], - "battery": [DEVICE_CLASS_BATTERY, "Battery", PERCENTAGE], -} +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="temperature", + name="Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + ), + SensorEntityDescription( + key="humidity", + name="Humidity", + device_class=DEVICE_CLASS_HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + ), + SensorEntityDescription( + key="battery", + name="Battery", + device_class=DEVICE_CLASS_BATTERY, + native_unit_of_measurement=PERCENTAGE, + ), +) + +SENSOR_KEYS = [desc.key for desc in SENSOR_TYPES] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_MAC): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] + vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( + cv.ensure_list, [vol.In(SENSOR_KEYS)] ), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_MEDIAN, default=DEFAULT_MEDIAN): cv.positive_int, @@ -73,79 +96,46 @@ def setup_platform(hass, config, add_entities, discovery_info=None): backend = BACKEND _LOGGER.debug("MiTempBt is using %s backend", backend.__name__) - cache = config.get(CONF_CACHE) + cache = config[CONF_CACHE] poller = mitemp_bt_poller.MiTempBtPoller( - config.get(CONF_MAC), + config[CONF_MAC], cache_timeout=cache, - adapter=config.get(CONF_ADAPTER), + adapter=config[CONF_ADAPTER], backend=backend, ) - force_update = config.get(CONF_FORCE_UPDATE) - median = config.get(CONF_MEDIAN) - poller.ble_timeout = config.get(CONF_TIMEOUT) - poller.retries = config.get(CONF_RETRIES) + prefix = config[CONF_NAME] + force_update = config[CONF_FORCE_UPDATE] + median = config[CONF_MEDIAN] + poller.ble_timeout = config[CONF_TIMEOUT] + poller.retries = config[CONF_RETRIES] - devs = [] + monitored_conditions = config[CONF_MONITORED_CONDITIONS] + entities = [ + MiTempBtSensor(poller, prefix, force_update, median, description) + for description in SENSOR_TYPES + if description.key in monitored_conditions + ] - for parameter in config[CONF_MONITORED_CONDITIONS]: - device = SENSOR_TYPES[parameter][0] - name = SENSOR_TYPES[parameter][1] - unit = SENSOR_TYPES[parameter][2] - - prefix = config.get(CONF_NAME) - if prefix: - name = f"{prefix} {name}" - - devs.append( - MiTempBtSensor(poller, parameter, device, name, unit, force_update, median) - ) - - add_entities(devs) + add_entities(entities) class MiTempBtSensor(SensorEntity): """Implementing the MiTempBt sensor.""" - def __init__(self, poller, parameter, device, name, unit, force_update, median): + def __init__( + self, poller, prefix, force_update, median, description: SensorEntityDescription + ): """Initialize the sensor.""" + self.entity_description = description self.poller = poller - self.parameter = parameter - self._device = device - self._unit = unit - self._name = name - self._state = None - self.data = [] - self._force_update = force_update + self.data: list[Any] = [] + self._attr_name = f"{prefix} {description.name}" + self._attr_force_update = force_update # Median is used to filter out outliers. median of 3 will filter # single outliers, while median of 5 will filter double outliers # Use median_count = 1 if no filtering is required. self.median_count = median - @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 units of measurement.""" - return self._unit - - @property - def device_class(self): - """Device class of this entity.""" - return self._device - - @property - def force_update(self): - """Force update.""" - return self._force_update - def update(self): """ Update current conditions. @@ -154,7 +144,7 @@ class MiTempBtSensor(SensorEntity): """ try: _LOGGER.debug("Polling data for %s", self.name) - data = self.poller.parameter_value(self.parameter) + data = self.poller.parameter_value(self.entity_description.key) except OSError as ioerr: _LOGGER.warning("Polling error %s", ioerr) return @@ -174,7 +164,7 @@ class MiTempBtSensor(SensorEntity): if self.data: self.data = self.data[1:] else: - self._state = None + self._attr_native_value = None return if len(self.data) > self.median_count: @@ -183,6 +173,6 @@ class MiTempBtSensor(SensorEntity): if len(self.data) == self.median_count: median = sorted(self.data)[int((self.median_count - 1) / 2)] _LOGGER.debug("Median is: %s", median) - self._state = median + self._attr_native_value = median else: _LOGGER.debug("Not yet enough data for median calculation") From af32bd956cf03b8ae49cddd87349575112572808 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 17 Aug 2021 01:30:32 +0200 Subject: [PATCH 258/355] Add DEVICE_CLASS_UPDATE to Binary Sensor (#53945) 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/binary_sensor/__init__.py | 4 ++++ .../components/binary_sensor/device_condition.py | 6 ++++++ homeassistant/components/binary_sensor/device_trigger.py | 5 +++++ homeassistant/components/binary_sensor/strings.json | 8 ++++++++ .../components/binary_sensor/translations/en.json | 8 ++++++++ 5 files changed, 31 insertions(+) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 2bd5de34d51..87d574fc4b0 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -92,6 +92,9 @@ DEVICE_CLASS_SMOKE = "smoke" # On means sound detected, Off means no sound (clear) DEVICE_CLASS_SOUND = "sound" +# On means update available, Off means up-to-date +DEVICE_CLASS_UPDATE = "update" + # On means vibration detected, Off means no vibration DEVICE_CLASS_VIBRATION = "vibration" @@ -121,6 +124,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, + DEVICE_CLASS_UPDATE, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, ] diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index eed5c3f5896..309e26847a1 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -37,6 +37,7 @@ from . import ( DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, + DEVICE_CLASS_UPDATE, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, DOMAIN, @@ -82,6 +83,8 @@ CONF_IS_SMOKE = "is_smoke" CONF_IS_NO_SMOKE = "is_no_smoke" CONF_IS_SOUND = "is_sound" CONF_IS_NO_SOUND = "is_no_sound" +CONF_IS_UPDATE = "is_update" +CONF_IS_NO_UPDATE = "is_no_update" CONF_IS_VIBRATION = "is_vibration" CONF_IS_NO_VIBRATION = "is_no_vibration" CONF_IS_OPEN = "is_open" @@ -107,6 +110,7 @@ IS_ON = [ CONF_IS_PROBLEM, CONF_IS_SMOKE, CONF_IS_SOUND, + CONF_IS_UPDATE, CONF_IS_UNSAFE, CONF_IS_VIBRATION, CONF_IS_ON, @@ -133,6 +137,7 @@ IS_OFF = [ CONF_IS_NO_PROBLEM, CONF_IS_NO_SMOKE, CONF_IS_NO_SOUND, + CONF_IS_NO_UPDATE, CONF_IS_NO_VIBRATION, CONF_IS_OFF, ] @@ -187,6 +192,7 @@ ENTITY_CONDITIONS = { DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_IS_UNSAFE}, {CONF_TYPE: CONF_IS_NOT_UNSAFE}], DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_IS_SMOKE}, {CONF_TYPE: CONF_IS_NO_SMOKE}], DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_IS_SOUND}, {CONF_TYPE: CONF_IS_NO_SOUND}], + DEVICE_CLASS_UPDATE: [{CONF_TYPE: CONF_IS_UPDATE}, {CONF_TYPE: CONF_IS_NO_UPDATE}], DEVICE_CLASS_VIBRATION: [ {CONF_TYPE: CONF_IS_VIBRATION}, {CONF_TYPE: CONF_IS_NO_VIBRATION}, diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index ad5c26ed04f..a0966b5a018 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -35,6 +35,7 @@ from . import ( DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, + DEVICE_CLASS_UPDATE, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, DOMAIN, @@ -82,6 +83,8 @@ CONF_SMOKE = "smoke" CONF_NO_SMOKE = "no_smoke" CONF_SOUND = "sound" CONF_NO_SOUND = "no_sound" +CONF_UPDATE = "update" +CONF_NO_UPDATE = "no_update" CONF_VIBRATION = "vibration" CONF_NO_VIBRATION = "no_vibration" CONF_OPENED = "opened" @@ -108,6 +111,7 @@ TURNED_ON = [ CONF_SMOKE, CONF_SOUND, CONF_UNSAFE, + CONF_UPDATE, CONF_VIBRATION, CONF_TURNED_ON, ] @@ -169,6 +173,7 @@ ENTITY_TRIGGERS = { DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_UNSAFE}, {CONF_TYPE: CONF_NOT_UNSAFE}], DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_SMOKE}, {CONF_TYPE: CONF_NO_SMOKE}], DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_SOUND}, {CONF_TYPE: CONF_NO_SOUND}], + DEVICE_CLASS_UPDATE: [{CONF_TYPE: CONF_UPDATE}, {CONF_TYPE: CONF_NO_UPDATE}], DEVICE_CLASS_VIBRATION: [ {CONF_TYPE: CONF_VIBRATION}, {CONF_TYPE: CONF_NO_VIBRATION}, diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json index 7380d1be576..62b6ec20323 100644 --- a/homeassistant/components/binary_sensor/strings.json +++ b/homeassistant/components/binary_sensor/strings.json @@ -38,6 +38,8 @@ "is_no_smoke": "{entity_name} is not detecting smoke", "is_sound": "{entity_name} is detecting sound", "is_no_sound": "{entity_name} is not detecting sound", + "is_update": "{entity_name} has an update available", + "is_no_update": "{entity_name} is up-to-date", "is_vibration": "{entity_name} is detecting vibration", "is_no_vibration": "{entity_name} is not detecting vibration", "is_open": "{entity_name} is open", @@ -82,6 +84,8 @@ "no_smoke": "{entity_name} stopped detecting smoke", "sound": "{entity_name} started detecting sound", "no_sound": "{entity_name} stopped detecting sound", + "update": "{entity_name} got an update available", + "no_update": "{entity_name} became up-to-date", "vibration": "{entity_name} started detecting vibration", "no_vibration": "{entity_name} stopped detecting vibration", "opened": "{entity_name} opened", @@ -175,6 +179,10 @@ "off": "[%key:component::binary_sensor::state::gas::off%]", "on": "[%key:component::binary_sensor::state::gas::on%]" }, + "update": { + "off": "Up-to-date", + "on": "Update available" + }, "vibration": { "off": "[%key:component::binary_sensor::state::gas::off%]", "on": "[%key:component::binary_sensor::state::gas::on%]" diff --git a/homeassistant/components/binary_sensor/translations/en.json b/homeassistant/components/binary_sensor/translations/en.json index 98c8a3a220a..047820498da 100644 --- a/homeassistant/components/binary_sensor/translations/en.json +++ b/homeassistant/components/binary_sensor/translations/en.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} is not detecting problem", "is_no_smoke": "{entity_name} is not detecting smoke", "is_no_sound": "{entity_name} is not detecting sound", + "is_no_update": "{entity_name} is up-to-date", "is_no_vibration": "{entity_name} is not detecting vibration", "is_not_bat_low": "{entity_name} battery is normal", "is_not_cold": "{entity_name} is not cold", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name} is detecting smoke", "is_sound": "{entity_name} is detecting sound", "is_unsafe": "{entity_name} is unsafe", + "is_update": "{entity_name} has an update available", "is_vibration": "{entity_name} is detecting vibration" }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name} stopped detecting problem", "no_smoke": "{entity_name} stopped detecting smoke", "no_sound": "{entity_name} stopped detecting sound", + "no_update": "{entity_name} became up-to-date", "no_vibration": "{entity_name} stopped detecting vibration", "not_bat_low": "{entity_name} battery normal", "not_cold": "{entity_name} became not cold", @@ -86,6 +89,7 @@ "turned_off": "{entity_name} turned off", "turned_on": "{entity_name} turned on", "unsafe": "{entity_name} became unsafe", + "update": "{entity_name} got an update available", "vibration": "{entity_name} started detecting vibration" } }, @@ -178,6 +182,10 @@ "off": "Clear", "on": "Detected" }, + "update": { + "off": "Up-to-date", + "on": "Update available" + }, "vibration": { "off": "Clear", "on": "Detected" From 1661de5c19875205c77ee427dea28909ebbbec03 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 17 Aug 2021 00:12:45 +0000 Subject: [PATCH 259/355] [ci skip] Translation update --- .../components/adax/translations/es.json | 11 ++++++++++ .../airvisual/translations/sensor.es.json | 20 +++++++++++++++++++ .../components/co2signal/translations/es.json | 20 +++++++++++++++++++ .../components/flipr/translations/es.json | 4 ++++ .../forecast_solar/translations/es.json | 13 ++++++++++++ .../components/honeywell/translations/es.json | 10 ++++++++++ .../nfandroidtv/translations/es.json | 10 ++++++++++ .../nmap_tracker/translations/en.json | 3 ++- .../components/sensor/translations/de.json | 2 ++ .../components/sensor/translations/en.json | 16 +++++++++++++++ .../components/sensor/translations/es.json | 2 ++ .../components/sensor/translations/hu.json | 2 ++ .../synology_dsm/translations/es.json | 4 ++++ .../components/tractive/translations/de.json | 3 ++- .../components/tractive/translations/en.json | 4 +++- .../components/tractive/translations/hu.json | 4 +++- .../tractive/translations/zh-Hant.json | 4 +++- .../xiaomi_miio/translations/no.json | 2 +- .../xiaomi_miio/translations/ru.json | 2 +- 19 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/adax/translations/es.json create mode 100644 homeassistant/components/airvisual/translations/sensor.es.json create mode 100644 homeassistant/components/co2signal/translations/es.json create mode 100644 homeassistant/components/honeywell/translations/es.json create mode 100644 homeassistant/components/nfandroidtv/translations/es.json diff --git a/homeassistant/components/adax/translations/es.json b/homeassistant/components/adax/translations/es.json new file mode 100644 index 00000000000..4a65e469bcd --- /dev/null +++ b/homeassistant/components/adax/translations/es.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "account_id": "ID de la cuenta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.es.json b/homeassistant/components/airvisual/translations/sensor.es.json new file mode 100644 index 00000000000..4a8a7cea1e3 --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.es.json @@ -0,0 +1,20 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "Mon\u00f3xido de carbono", + "n2": "Di\u00f3xido de nitr\u00f3geno", + "o3": "Ozono", + "p1": "PM10", + "p2": "PM2.5", + "s2": "Di\u00f3xido de azufre" + }, + "airvisual__pollutant_level": { + "good": "Bien", + "hazardous": "Peligroso", + "moderate": "Moderado", + "unhealthy": "Insalubre", + "unhealthy_sensitive": "Incorrecto para grupos sensibles", + "very_unhealthy": "Muy poco saludable" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/es.json b/homeassistant/components/co2signal/translations/es.json new file mode 100644 index 00000000000..071ae642c74 --- /dev/null +++ b/homeassistant/components/co2signal/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "api_ratelimit": "Se ha superado el l\u00edmite de velocidad de la API" + }, + "step": { + "country": { + "data": { + "country_code": "C\u00f3digo del pa\u00eds" + } + }, + "user": { + "data": { + "location": "Obtener datos para" + }, + "description": "Visite https://co2signal.com/ para solicitar un token." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/es.json b/homeassistant/components/flipr/translations/es.json index 56898d19a42..766f83856ec 100644 --- a/homeassistant/components/flipr/translations/es.json +++ b/homeassistant/components/flipr/translations/es.json @@ -1,10 +1,14 @@ { "config": { "error": { + "no_flipr_id_found": "Por ahora no hay ning\u00fan ID de Flipr asociado a tu cuenta. Deber\u00edas verificar que est\u00e1 funcionando con la aplicaci\u00f3n m\u00f3vil de Flipr primero.", "unknown": "Error desconocido" }, "step": { "flipr_id": { + "data": { + "flipr_id": "ID de Flipr" + }, "description": "Elija su ID de Flipr en la lista", "title": "Elige tu Flipr" }, diff --git a/homeassistant/components/forecast_solar/translations/es.json b/homeassistant/components/forecast_solar/translations/es.json index 2189cb91f77..8a1b51a5084 100644 --- a/homeassistant/components/forecast_solar/translations/es.json +++ b/homeassistant/components/forecast_solar/translations/es.json @@ -1,8 +1,21 @@ { + "config": { + "step": { + "user": { + "data": { + "azimuth": "Acimut (360 grados, 0 = Norte, 90 = Este, 180 = Sur, 270 = Oeste)", + "declination": "Declinaci\u00f3n (0 = Horizontal, 90 = Vertical)", + "modules power": "Potencia total en vatios pico de sus m\u00f3dulos solares" + }, + "description": "Rellene los datos de sus paneles solares. Consulte la documentaci\u00f3n si alg\u00fan campo no est\u00e1 claro." + } + } + }, "options": { "step": { "init": { "data": { + "api_key": "Clave API de Forecast.Solar (opcional)", "azimuth": "Azimut (360 grados, 0 = Norte, 90 = Este, 180 = Sur, 270 = Oeste)", "damping": "Factor de amortiguaci\u00f3n: ajusta los resultados por la ma\u00f1ana y por la noche", "declination": "Declinaci\u00f3n (0 = Horizontal, 90 = Vertical)", diff --git a/homeassistant/components/honeywell/translations/es.json b/homeassistant/components/honeywell/translations/es.json new file mode 100644 index 00000000000..41534be9d8d --- /dev/null +++ b/homeassistant/components/honeywell/translations/es.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "Por favor, introduzca las credenciales utilizadas para iniciar sesi\u00f3n en mytotalconnectcomfort.com.", + "title": "Honeywell Total Connect Comfort (US)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/es.json b/homeassistant/components/nfandroidtv/translations/es.json new file mode 100644 index 00000000000..e99ce545b74 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/es.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "Esta integraci\u00f3n requiere la aplicaci\u00f3n de Notificaciones para Android TV.\n\nPara Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nPara Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\nDebe configurar una reserva DHCP en su router (consulte el manual de usuario de su router) o una direcci\u00f3n IP est\u00e1tica en el dispositivo. Si no, el dispositivo acabar\u00e1 por no estar disponible.", + "title": "Notificaciones para Android TV / Fire TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/en.json b/homeassistant/components/nmap_tracker/translations/en.json index 985225414a6..6b83532a0e2 100644 --- a/homeassistant/components/nmap_tracker/translations/en.json +++ b/homeassistant/components/nmap_tracker/translations/en.json @@ -29,7 +29,8 @@ "home_interval": "Minimum number of minutes between scans of active devices (preserve battery)", "hosts": "Network addresses (comma seperated) to scan", "interval_seconds": "Scan interval", - "scan_options": "Raw configurable scan options for Nmap" + "scan_options": "Raw configurable scan options for Nmap", + "track_new_devices": "Track new devices" }, "description": "Configure hosts to be scanned by Nmap. Network address and excludes can be IP Addresses (192.168.1.1), IP Networks (192.168.0.0/24) or IP Ranges (192.168.1.0-32)." } diff --git a/homeassistant/components/sensor/translations/de.json b/homeassistant/components/sensor/translations/de.json index 4f16c07be01..c65959b8210 100644 --- a/homeassistant/components/sensor/translations/de.json +++ b/homeassistant/components/sensor/translations/de.json @@ -6,6 +6,7 @@ "is_carbon_monoxide": "Aktuelle {entity_name} Kohlenstoffmonoxid-Konzentration", "is_current": "Aktueller Strom von {entity_name}", "is_energy": "Aktuelle Energie von {entity_name}", + "is_gas": "Aktuelles {entity_name} Gas", "is_humidity": "{entity_name} Feuchtigkeit", "is_illuminance": "Aktuelle {entity_name} Helligkeit", "is_power": "Aktuelle {entity_name} Leistung", @@ -22,6 +23,7 @@ "carbon_monoxide": "{entity_name} Kohlenstoffmonoxid-Konzentrations\u00e4nderung", "current": "{entity_name} Stromver\u00e4nderung", "energy": "{entity_name} Energie\u00e4nderungen", + "gas": "{entity_name} Gas\u00e4nderungen", "humidity": "{entity_name} Feuchtigkeits\u00e4nderungen", "illuminance": "{entity_name} Helligkeits\u00e4nderungen", "power": "{entity_name} Leistungs\u00e4nderungen", diff --git a/homeassistant/components/sensor/translations/en.json b/homeassistant/components/sensor/translations/en.json index 69737c7c93a..5fa23a334cb 100644 --- a/homeassistant/components/sensor/translations/en.json +++ b/homeassistant/components/sensor/translations/en.json @@ -9,10 +9,18 @@ "is_gas": "Current {entity_name} gas", "is_humidity": "Current {entity_name} humidity", "is_illuminance": "Current {entity_name} illuminance", + "is_nitrogen_dioxide": "Current {entity_name} nitrogen dioxide concentration level", + "is_nitrogen_monoxide": "Current {entity_name} nitrogen monoxide concentration level", + "is_nitrous_oxide": "Current {entity_name} nitrous oxide concentration level", + "is_ozone": "Current {entity_name} ozone concentration level", + "is_pm1": "Current {entity_name} PM1 concentration level", + "is_pm10": "Current {entity_name} PM10 concentration level", + "is_pm25": "Current {entity_name} PM2.5 concentration level", "is_power": "Current {entity_name} power", "is_power_factor": "Current {entity_name} power factor", "is_pressure": "Current {entity_name} pressure", "is_signal_strength": "Current {entity_name} signal strength", + "is_sulphur_dioxide": "Current {entity_name} sulphur dioxide concentration level", "is_temperature": "Current {entity_name} temperature", "is_value": "Current {entity_name} value", "is_voltage": "Current {entity_name} voltage" @@ -26,10 +34,18 @@ "gas": "{entity_name} gas changes", "humidity": "{entity_name} humidity changes", "illuminance": "{entity_name} illuminance changes", + "nitrogen_dioxide": "{entity_name} nitrogen dioxide concentration changes", + "nitrogen_monoxide": "{entity_name} nitrogen monoxide concentration changes", + "nitrous_oxide": "{entity_name} nitrous oxide concentration changes", + "ozone": "{entity_name} ozone concentration changes", + "pm1": "{entity_name} PM1 concentration changes", + "pm10": "{entity_name} PM10 concentration changes", + "pm25": "{entity_name} PM2.5 concentration changes", "power": "{entity_name} power changes", "power_factor": "{entity_name} power factor changes", "pressure": "{entity_name} pressure changes", "signal_strength": "{entity_name} signal strength changes", + "sulphur_dioxide": "{entity_name} sulphur dioxide concentration changes", "temperature": "{entity_name} temperature changes", "value": "{entity_name} value changes", "voltage": "{entity_name} voltage changes" diff --git a/homeassistant/components/sensor/translations/es.json b/homeassistant/components/sensor/translations/es.json index da96c7d92db..48c61f321a1 100644 --- a/homeassistant/components/sensor/translations/es.json +++ b/homeassistant/components/sensor/translations/es.json @@ -6,6 +6,7 @@ "is_carbon_monoxide": "Nivel actual de concentraci\u00f3n de mon\u00f3xido de carbono {entity_name}", "is_current": "Corriente actual de {entity_name}", "is_energy": "Energ\u00eda actual de {entity_name}", + "is_gas": "Gas actual de {entity_name}", "is_humidity": "Humedad actual de {entity_name}", "is_illuminance": "Luminosidad actual de {entity_name}", "is_power": "Potencia actual de {entity_name}", @@ -22,6 +23,7 @@ "carbon_monoxide": "{entity_name} cambios en la concentraci\u00f3n de mon\u00f3xido de carbono", "current": "Cambio de corriente en {entity_name}", "energy": "Cambio de energ\u00eda en {entity_name}", + "gas": "Cambio de gas de {entity_name}", "humidity": "Cambios de humedad de {entity_name}", "illuminance": "Cambios de luminosidad de {entity_name}", "power": "Cambios de potencia de {entity_name}", diff --git a/homeassistant/components/sensor/translations/hu.json b/homeassistant/components/sensor/translations/hu.json index 9b1c9bece82..1e2aba465cc 100644 --- a/homeassistant/components/sensor/translations/hu.json +++ b/homeassistant/components/sensor/translations/hu.json @@ -6,6 +6,7 @@ "is_carbon_monoxide": "Jelenlegi {entity_name} sz\u00e9n-monoxid koncentr\u00e1ci\u00f3 szint", "is_current": "Jelenlegi {entity_name} \u00e1ram", "is_energy": "A jelenlegi {entity_name} energia", + "is_gas": "Jelenlegi {entity_name} g\u00e1z", "is_humidity": "{entity_name} aktu\u00e1lis p\u00e1ratartalma", "is_illuminance": "{entity_name} aktu\u00e1lis megvil\u00e1g\u00edt\u00e1sa", "is_power": "{entity_name} aktu\u00e1lis teljes\u00edtm\u00e9nye", @@ -22,6 +23,7 @@ "carbon_monoxide": "{entity_name} sz\u00e9n-monoxid koncentr\u00e1ci\u00f3ja megv\u00e1ltozik", "current": "{entity_name} aktu\u00e1lis v\u00e1ltoz\u00e1sai", "energy": "{entity_name} energiav\u00e1ltoz\u00e1sa", + "gas": "{entity_name} g\u00e1z v\u00e1ltoz\u00e1sok", "humidity": "{entity_name} p\u00e1ratartalma v\u00e1ltozik", "illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1sa v\u00e1ltozik", "power": "{entity_name} teljes\u00edtm\u00e9nye v\u00e1ltozik", diff --git a/homeassistant/components/synology_dsm/translations/es.json b/homeassistant/components/synology_dsm/translations/es.json index f76ce7ab27a..7b86c248110 100644 --- a/homeassistant/components/synology_dsm/translations/es.json +++ b/homeassistant/components/synology_dsm/translations/es.json @@ -29,6 +29,10 @@ "description": "\u00bfQuieres configurar {name} ({host})?", "title": "Synology DSM" }, + "reauth": { + "description": "Raz\u00f3n: {details}", + "title": "Synology DSM Volver a autenticar la integraci\u00f3n" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/tractive/translations/de.json b/homeassistant/components/tractive/translations/de.json index 522649fe393..fbb3411a6c5 100644 --- a/homeassistant/components/tractive/translations/de.json +++ b/homeassistant/components/tractive/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "invalid_auth": "Ung\u00fcltige Authentifizierung", diff --git a/homeassistant/components/tractive/translations/en.json b/homeassistant/components/tractive/translations/en.json index c85034b0729..dcb3a128ac4 100644 --- a/homeassistant/components/tractive/translations/en.json +++ b/homeassistant/components/tractive/translations/en.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "reauth_failed_existing": "Could not update the config entry, please remove the integration and set it up again.", + "reauth_successful": "Re-authentication was successful" }, "error": { "invalid_auth": "Invalid authentication", diff --git a/homeassistant/components/tractive/translations/hu.json b/homeassistant/components/tractive/translations/hu.json index 8830cb61711..d0f75a28ed0 100644 --- a/homeassistant/components/tractive/translations/hu.json +++ b/homeassistant/components/tractive/translations/hu.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "reauth_failed_existing": "Nem siker\u00fclt friss\u00edteni a konfigur\u00e1ci\u00f3s bejegyz\u00e9st. K\u00e9rj\u00fck, t\u00e1vol\u00edtsa el az integr\u00e1ci\u00f3t, \u00e9s \u00e1ll\u00edtsa be \u00fajra.", + "reauth_successful": "Az ism\u00e9telt hiteles\u00edt\u00e9s sikeres volt" }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", diff --git a/homeassistant/components/tractive/translations/zh-Hant.json b/homeassistant/components/tractive/translations/zh-Hant.json index 64aba47b6b8..8c9ec055f63 100644 --- a/homeassistant/components/tractive/translations/zh-Hant.json +++ b/homeassistant/components/tractive/translations/zh-Hant.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_failed_existing": "\u7121\u6cd5\u66f4\u65b0\u8a2d\u5b9a\u5be6\u9ad4\uff0c\u8acb\u79fb\u9664\u6574\u5408\u4e26\u91cd\u65b0\u8a2d\u5b9a\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", diff --git a/homeassistant/components/xiaomi_miio/translations/no.json b/homeassistant/components/xiaomi_miio/translations/no.json index 8fa93169647..a296dd7aa08 100644 --- a/homeassistant/components/xiaomi_miio/translations/no.json +++ b/homeassistant/components/xiaomi_miio/translations/no.json @@ -10,7 +10,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes", "cloud_credentials_incomplete": "Utskriftsinformasjon for skyen er fullstendig. Fyll ut brukernavn, passord og land", - "cloud_login_error": "Kunne ikke logge p\u00e5 Xioami Miio Cloud, sjekk legitimasjonen.", + "cloud_login_error": "Kunne ikke logge inn p\u00e5 Xiaomi Miio Cloud, sjekk legitimasjonen.", "cloud_no_devices": "Ingen enheter funnet i denne Xiaomi Miio-skykontoen.", "no_device_selected": "Ingen enhet valgt, vennligst velg en enhet.", "unknown_device": "Enhetsmodellen er ikke kjent, kan ikke konfigurere enheten ved hjelp av konfigurasjonsflyt." diff --git a/homeassistant/components/xiaomi_miio/translations/ru.json b/homeassistant/components/xiaomi_miio/translations/ru.json index f9aeb824b20..017660e51c6 100644 --- a/homeassistant/components/xiaomi_miio/translations/ru.json +++ b/homeassistant/components/xiaomi_miio/translations/ru.json @@ -10,7 +10,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.", "cloud_credentials_incomplete": "\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u043e\u0431\u043b\u0430\u043a\u0435 \u043d\u0435\u043f\u043e\u043b\u043d\u044b\u0435. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f, \u043f\u0430\u0440\u043e\u043b\u044c \u0438 \u0441\u0442\u0440\u0430\u043d\u0443.", - "cloud_login_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u043e\u0439\u0442\u0438 \u0432 Xioami Miio Cloud, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "cloud_login_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u043e\u0439\u0442\u0438 \u0432 Xiaomi Miio Cloud, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "cloud_no_devices": "\u0412 \u044d\u0442\u043e\u0439 \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Xiaomi Miio \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b.", "no_device_selected": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u043d\u043e \u0438\u0437 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.", "unknown_device": "\u041c\u043e\u0434\u0435\u043b\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430, \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043c\u0430\u0441\u0442\u0435\u0440\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." From 476f3b5cb543eb00a43c6f4610758c1d750e019c Mon Sep 17 00:00:00 2001 From: Bert Roos Date: Tue, 17 Aug 2021 05:20:16 +0200 Subject: [PATCH 260/355] Fix Google Calendar event loading (#54231) --- homeassistant/components/google/__init__.py | 23 +++++++++-------- homeassistant/components/google/calendar.py | 28 +++++++++++++-------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 6cc7221ba1d..33afac6f57b 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -108,16 +108,19 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -_SINGLE_CALSEARCH_CONFIG = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Optional(CONF_IGNORE_AVAILABILITY, default=True): cv.boolean, - vol.Optional(CONF_OFFSET): cv.string, - vol.Optional(CONF_SEARCH): cv.string, - vol.Optional(CONF_TRACK): cv.boolean, - vol.Optional(CONF_MAX_RESULTS): cv.positive_int, - } +_SINGLE_CALSEARCH_CONFIG = vol.All( + cv.deprecated(CONF_MAX_RESULTS), + vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Optional(CONF_IGNORE_AVAILABILITY, default=True): cv.boolean, + vol.Optional(CONF_OFFSET): cv.string, + vol.Optional(CONF_SEARCH): cv.string, + vol.Optional(CONF_TRACK): cv.boolean, + vol.Optional(CONF_MAX_RESULTS): cv.positive_int, # Now unused + } + ), ) DEVICE_SCHEMA = vol.Schema( diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 2cc66121948..5c06e0fbb94 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -18,7 +18,6 @@ from homeassistant.util import Throttle, dt from . import ( CONF_CAL_ID, CONF_IGNORE_AVAILABILITY, - CONF_MAX_RESULTS, CONF_SEARCH, CONF_TRACK, DEFAULT_CONF_OFFSET, @@ -30,7 +29,6 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_GOOGLE_SEARCH_PARAMS = { "orderBy": "startTime", - "maxResults": 5, "singleEvents": True, } @@ -71,7 +69,6 @@ class GoogleCalendarEventDevice(CalendarEventDevice): calendar, data.get(CONF_SEARCH), data.get(CONF_IGNORE_AVAILABILITY), - data.get(CONF_MAX_RESULTS), ) self._event = None self._name = data[CONF_NAME] @@ -113,15 +110,12 @@ class GoogleCalendarEventDevice(CalendarEventDevice): class GoogleCalendarData: """Class to utilize calendar service object to get next event.""" - def __init__( - self, calendar_service, calendar_id, search, ignore_availability, max_results - ): + def __init__(self, calendar_service, calendar_id, search, ignore_availability): """Set up how we are going to search the google calendar.""" self.calendar_service = calendar_service self.calendar_id = calendar_id self.search = search self.ignore_availability = ignore_availability - self.max_results = max_results self.event = None def _prepare_query(self): @@ -132,8 +126,8 @@ class GoogleCalendarData: return None, None params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS) params["calendarId"] = self.calendar_id - if self.max_results: - params["maxResults"] = self.max_results + params["maxResults"] = 100 # Page size + if self.search: params["q"] = self.search @@ -147,18 +141,30 @@ class GoogleCalendarData: params["timeMin"] = start_date.isoformat("T") params["timeMax"] = end_date.isoformat("T") + event_list = [] events = await hass.async_add_executor_job(service.events) + page_token = None + while True: + page_token = await self.async_get_events_page( + hass, events, params, page_token, event_list + ) + if not page_token: + break + return event_list + + async def async_get_events_page(self, hass, events, params, page_token, event_list): + """Get a page of events in a specific time frame.""" + params["pageToken"] = page_token result = await hass.async_add_executor_job(events.list(**params).execute) items = result.get("items", []) - event_list = [] for item in items: if not self.ignore_availability and "transparency" in item: if item["transparency"] == "opaque": event_list.append(item) else: event_list.append(item) - return event_list + return result.get("nextPageToken") @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): From e0a8ec4f62afef83e015f470bd7348c34bbbdccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 17 Aug 2021 08:30:35 +0200 Subject: [PATCH 261/355] Add device class update to the updater binary_sensor (#54732) --- .../components/updater/binary_sensor.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/updater/binary_sensor.py b/homeassistant/components/updater/binary_sensor.py index 1c6bacede62..25339f6308a 100644 --- a/homeassistant/components/updater/binary_sensor.py +++ b/homeassistant/components/updater/binary_sensor.py @@ -1,7 +1,10 @@ """Support for Home Assistant Updater binary sensors.""" from __future__ import annotations -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_UPDATE, + BinarySensorEntity, +) from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import ATTR_NEWEST_VERSION, ATTR_RELEASE_NOTES, DOMAIN as UPDATER_DOMAIN @@ -18,15 +21,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class UpdaterBinary(CoordinatorEntity, BinarySensorEntity): """Representation of an updater binary sensor.""" - @property - def name(self) -> str: - """Return the name of the binary sensor, if any.""" - return "Updater" - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return "updater" + _attr_device_class = DEVICE_CLASS_UPDATE + _attr_name = "Updater" + _attr_unique_id = "updater" @property def is_on(self) -> bool | None: From 789e6555cc8746c666e7249f2f2f1bbce9d73452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 17 Aug 2021 08:30:55 +0200 Subject: [PATCH 262/355] Add device class update to hassio update entities (#54733) --- homeassistant/components/hassio/binary_sensor.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/binary_sensor.py b/homeassistant/components/hassio/binary_sensor.py index 01930b5ec0e..7345dd4a000 100644 --- a/homeassistant/components/hassio/binary_sensor.py +++ b/homeassistant/components/hassio/binary_sensor.py @@ -1,7 +1,10 @@ """Binary sensor platform for Hass.io addons.""" from __future__ import annotations -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_UPDATE, + BinarySensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -35,6 +38,8 @@ async def async_setup_entry( class HassioAddonBinarySensor(HassioAddonEntity, BinarySensorEntity): """Binary sensor to track whether an update is available for a Hass.io add-on.""" + _attr_device_class = DEVICE_CLASS_UPDATE + @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" @@ -44,6 +49,8 @@ class HassioAddonBinarySensor(HassioAddonEntity, BinarySensorEntity): class HassioOSBinarySensor(HassioOSEntity, BinarySensorEntity): """Binary sensor to track whether an update is available for Hass.io OS.""" + _attr_device_class = DEVICE_CLASS_UPDATE + @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" From 4c5d5a8f5a4402cb7c908d6016041ff0d001d374 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 Aug 2021 08:34:41 +0200 Subject: [PATCH 263/355] Update deCONZ to use new state classes (#54729) --- homeassistant/components/deconz/sensor.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index a741a2d37c1..012e686534f 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -17,6 +17,7 @@ from pydeconz.sensor import ( from homeassistant.components.sensor import ( DOMAIN, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.const import ( @@ -41,7 +42,6 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.util import dt as dt_util from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice @@ -68,7 +68,7 @@ ICON = { } STATE_CLASS = { - Consumption: STATE_CLASS_MEASUREMENT, + Consumption: STATE_CLASS_TOTAL_INCREASING, Humidity: STATE_CLASS_MEASUREMENT, Pressure: STATE_CLASS_MEASUREMENT, Temperature: STATE_CLASS_MEASUREMENT, @@ -164,9 +164,6 @@ class DeconzSensor(DeconzDevice, SensorEntity): type(self._device) ) - if device.type in Consumption.ZHATYPE: - self._attr_last_reset = dt_util.utc_from_timestamp(0) - @callback def async_update_callback(self, force_update=False): """Update the sensor's state.""" From afade22feb2ed439e70a1bbfc4b6288de9b8bdc7 Mon Sep 17 00:00:00 2001 From: Andre Richter Date: Tue, 17 Aug 2021 10:05:28 +0200 Subject: [PATCH 264/355] Add state classes to Vallox sensors (#54297) --- homeassistant/components/vallox/sensor.py | 25 +++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 836931f089e..dd669e156cf 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta import logging -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_CO2, @@ -44,6 +44,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_class=None, unit_of_measurement=PERCENTAGE, icon="mdi:fan", + state_class=STATE_CLASS_MEASUREMENT, ), ValloxSensor( name=f"{name} Extract Air", @@ -52,6 +53,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_class=DEVICE_CLASS_TEMPERATURE, unit_of_measurement=TEMP_CELSIUS, icon=None, + state_class=STATE_CLASS_MEASUREMENT, ), ValloxSensor( name=f"{name} Exhaust Air", @@ -60,6 +62,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_class=DEVICE_CLASS_TEMPERATURE, unit_of_measurement=TEMP_CELSIUS, icon=None, + state_class=STATE_CLASS_MEASUREMENT, ), ValloxSensor( name=f"{name} Outdoor Air", @@ -68,6 +71,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_class=DEVICE_CLASS_TEMPERATURE, unit_of_measurement=TEMP_CELSIUS, icon=None, + state_class=STATE_CLASS_MEASUREMENT, ), ValloxSensor( name=f"{name} Supply Air", @@ -76,6 +80,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_class=DEVICE_CLASS_TEMPERATURE, unit_of_measurement=TEMP_CELSIUS, icon=None, + state_class=STATE_CLASS_MEASUREMENT, ), ValloxSensor( name=f"{name} Humidity", @@ -84,6 +89,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_class=DEVICE_CLASS_HUMIDITY, unit_of_measurement=PERCENTAGE, icon=None, + state_class=STATE_CLASS_MEASUREMENT, ), ValloxFilterRemainingSensor( name=f"{name} Remaining Time For Filter", @@ -100,6 +106,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_class=None, unit_of_measurement=PERCENTAGE, icon="mdi:gauge", + state_class=STATE_CLASS_MEASUREMENT, ), ValloxSensor( name=f"{name} CO2", @@ -108,6 +115,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_class=DEVICE_CLASS_CO2, unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, icon=None, + state_class=STATE_CLASS_MEASUREMENT, ), ] @@ -118,13 +126,21 @@ class ValloxSensor(SensorEntity): """Representation of a Vallox sensor.""" def __init__( - self, name, state_proxy, metric_key, device_class, unit_of_measurement, icon + self, + name, + state_proxy, + metric_key, + device_class, + unit_of_measurement, + icon, + state_class=None, ) -> None: """Initialize the Vallox sensor.""" self._name = name self._state_proxy = state_proxy self._metric_key = metric_key self._device_class = device_class + self._state_class = state_class self._unit_of_measurement = unit_of_measurement self._icon = icon self._available = None @@ -150,6 +166,11 @@ class ValloxSensor(SensorEntity): """Return the device class.""" return self._device_class + @property + def state_class(self): + """Return the state class.""" + return self._state_class + @property def icon(self): """Return the icon.""" From 69bc6bbe489b175bec6441ccc831d6522b77bd67 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 17 Aug 2021 10:10:56 +0200 Subject: [PATCH 265/355] Activate mypy for google_pubsub (#54649) --- .coveragerc | 1 + homeassistant/components/google_pubsub/__init__.py | 9 ++++++--- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.coveragerc b/.coveragerc index 4b5c0820650..e25f664efe8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -375,6 +375,7 @@ omit = homeassistant/components/google/* homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py + homeassistant/components/google_pubsub/__init__.py homeassistant/components/google_travel_time/__init__.py homeassistant/components/google_travel_time/helpers.py homeassistant/components/google_travel_time/sensor.py diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index 514b919e877..19530d9d663 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -45,9 +45,12 @@ def setup(hass: HomeAssistant, yaml_config: dict[str, Any]): config = yaml_config[DOMAIN] project_id = config[CONF_PROJECT_ID] topic_name = config[CONF_TOPIC_NAME] - service_principal_path = os.path.join( - hass.config.config_dir, config[CONF_SERVICE_PRINCIPAL] - ) + if hass.config.config_dir: + service_principal_path = os.path.join( + hass.config.config_dir, config[CONF_SERVICE_PRINCIPAL] + ) + else: + service_principal_path = config[CONF_SERVICE_PRINCIPAL] if not os.path.isfile(service_principal_path): _LOGGER.error("Path to credentials file cannot be found") diff --git a/mypy.ini b/mypy.ini index 837dc73343a..3108f73a49e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1367,9 +1367,6 @@ ignore_errors = true [mypy-homeassistant.components.google_assistant.*] ignore_errors = true -[mypy-homeassistant.components.google_pubsub.*] -ignore_errors = true - [mypy-homeassistant.components.gpmdp.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 513be3e59c2..6a863355afc 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -48,7 +48,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.geniushub.*", "homeassistant.components.glances.*", "homeassistant.components.google_assistant.*", - "homeassistant.components.google_pubsub.*", "homeassistant.components.gpmdp.*", "homeassistant.components.gree.*", "homeassistant.components.growatt_server.*", From 6d7ad8903f74959c6e3309907c77bc256bff5555 Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Tue, 17 Aug 2021 09:19:44 +0100 Subject: [PATCH 266/355] Energy support for Solax inverters (#54654) Co-authored-by: Franck Nijhof --- homeassistant/components/solax/sensor.py | 51 ++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index 4d1652e8b12..7854142c32b 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -6,8 +6,23 @@ from solax import real_time_api from solax.inverter import InverterError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, +) +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.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval @@ -34,10 +49,28 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_track_time_interval(hass, endpoint.async_refresh, SCAN_INTERVAL) devices = [] 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 unit = TEMP_CELSIUS + elif unit == "kWh": + device_class = DEVICE_CLASS_ENERGY + state_class = STATE_CLASS_TOTAL_INCREASING + elif unit == "V": + device_class = DEVICE_CLASS_VOLTAGE + state_class = STATE_CLASS_MEASUREMENT + elif unit == "A": + device_class = DEVICE_CLASS_CURRENT + state_class = STATE_CLASS_MEASUREMENT + elif unit == "W": + device_class = DEVICE_CLASS_POWER + state_class = STATE_CLASS_MEASUREMENT + elif unit == "%": + device_class = DEVICE_CLASS_BATTERY + state_class = STATE_CLASS_MEASUREMENT uid = f"{serial}-{idx}" - devices.append(Inverter(uid, serial, sensor, unit)) + devices.append(Inverter(uid, serial, sensor, unit, state_class, device_class)) endpoint.sensors = devices async_add_entities(devices) @@ -75,13 +108,23 @@ class RealTimeDataEndpoint: class Inverter(SensorEntity): """Class for a sensor.""" - def __init__(self, uid, serial, key, unit): + def __init__( + self, + uid, + serial, + key, + unit, + state_class=None, + device_class=None, + ): """Initialize an inverter sensor.""" self.uid = uid self.serial = serial self.key = key self.value = None self.unit = unit + self._attr_state_class = state_class + self._attr_device_class = device_class @property def native_value(self): From 4f3d1c5e126223c2ec6a18deb42be265338f9098 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 17 Aug 2021 10:49:22 +0200 Subject: [PATCH 267/355] Use PM1, PM25 and PM10 device classes in Nettigo Air Monitor integration (#54741) --- homeassistant/components/nam/const.py | 15 +++++++++------ tests/components/nam/test_sensor.py | 15 +++++++++------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py index a9d044f2c1d..da4831de9e5 100644 --- a/homeassistant/components/nam/const.py +++ b/homeassistant/components/nam/const.py @@ -13,6 +13,9 @@ from homeassistant.const import ( 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, @@ -122,14 +125,14 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( key=ATTR_SDS011_P1, name=f"{DEFAULT_NAME} SDS011 Particulate Matter 10", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - icon="mdi:blur", + device_class=DEVICE_CLASS_PM10, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_SDS011_P2, name=f"{DEFAULT_NAME} SDS011 Particulate Matter 2.5", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - icon="mdi:blur", + device_class=DEVICE_CLASS_PM25, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( @@ -150,28 +153,28 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( key=ATTR_SPS30_P0, name=f"{DEFAULT_NAME} SPS30 Particulate Matter 1.0", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - icon="mdi:blur", + device_class=DEVICE_CLASS_PM1, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_SPS30_P1, name=f"{DEFAULT_NAME} SPS30 Particulate Matter 10", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - icon="mdi:blur", + device_class=DEVICE_CLASS_PM10, state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_SPS30_P2, name=f"{DEFAULT_NAME} SPS30 Particulate Matter 2.5", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - icon="mdi:blur", + device_class=DEVICE_CLASS_PM25, state_class=STATE_CLASS_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:blur", + icon="mdi:molecule", state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index c5850ce719d..ce9a221007a 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -19,6 +19,9 @@ from homeassistant.const import ( 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, @@ -212,12 +215,12 @@ 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_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" entry = registry.async_get( "sensor.nettigo_air_monitor_sds011_particulate_matter_10" @@ -228,12 +231,12 @@ 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_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" entry = registry.async_get( "sensor.nettigo_air_monitor_sds011_particulate_matter_2_5" @@ -244,12 +247,12 @@ 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_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" entry = registry.async_get( "sensor.nettigo_air_monitor_sps30_particulate_matter_1_0" @@ -260,12 +263,12 @@ 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_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" entry = registry.async_get("sensor.nettigo_air_monitor_sps30_particulate_matter_10") assert entry @@ -274,12 +277,12 @@ 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_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" entry = registry.async_get( "sensor.nettigo_air_monitor_sps30_particulate_matter_2_5" @@ -295,7 +298,7 @@ async def test_sensor(hass): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_ICON) == "mdi:molecule" entry = registry.async_get( "sensor.nettigo_air_monitor_sps30_particulate_matter_4_0" From f1f05cdf1b7a75b7f76d09e43c1749fa17c8768d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 17 Aug 2021 11:57:45 +0200 Subject: [PATCH 268/355] Use DEVICE_CLASS_UPDATE in Shelly integration (#54746) --- homeassistant/components/shelly/binary_sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index dd1b3a9d66d..96d62152830 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_POWER, DEVICE_CLASS_PROBLEM, DEVICE_CLASS_SMOKE, + DEVICE_CLASS_UPDATE, DEVICE_CLASS_VIBRATION, STATE_ON, BinarySensorEntity, @@ -99,7 +100,7 @@ REST_SENSORS: Final = { ), "fwupdate": RestAttributeDescription( name="Firmware Update", - icon="mdi:update", + device_class=DEVICE_CLASS_UPDATE, value=lambda status, _: status["update"]["has_update"], default_enabled=False, extra_state_attributes=lambda status: { From a2c9cfbf415bfe10afc64a08a7999ae38ee90c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 17 Aug 2021 12:14:14 +0200 Subject: [PATCH 269/355] Use entity descriptions for hassio entities (#54749) --- .../components/hassio/binary_sensor.py | 52 ++++++++----- homeassistant/components/hassio/const.py | 5 +- homeassistant/components/hassio/entity.py | 75 +++---------------- homeassistant/components/hassio/sensor.py | 48 ++++++++---- 4 files changed, 86 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/hassio/binary_sensor.py b/homeassistant/components/hassio/binary_sensor.py index 7345dd4a000..dfd13adbde6 100644 --- a/homeassistant/components/hassio/binary_sensor.py +++ b/homeassistant/components/hassio/binary_sensor.py @@ -4,15 +4,25 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( DEVICE_CLASS_UPDATE, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ADDONS_COORDINATOR -from .const import ATTR_UPDATE_AVAILABLE +from .const import ATTR_UPDATE_AVAILABLE, DATA_KEY_ADDONS, DATA_KEY_OS from .entity import HassioAddonEntity, HassioOSEntity +ENTITY_DESCRIPTIONS = ( + BinarySensorEntityDescription( + device_class=DEVICE_CLASS_UPDATE, + entity_registry_enabled_default=False, + key=ATTR_UPDATE_AVAILABLE, + name="Update Available", + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -22,36 +32,44 @@ async def async_setup_entry( """Binary sensor set up for Hass.io config entry.""" coordinator = hass.data[ADDONS_COORDINATOR] - entities = [ - HassioAddonBinarySensor( - coordinator, addon, ATTR_UPDATE_AVAILABLE, "Update Available" - ) - for addon in coordinator.data["addons"].values() - ] - if coordinator.is_hass_os: - entities.append( - HassioOSBinarySensor(coordinator, ATTR_UPDATE_AVAILABLE, "Update Available") - ) + entities = [] + + for entity_description in ENTITY_DESCRIPTIONS: + for addon in coordinator.data[DATA_KEY_ADDONS].values(): + entities.append( + HassioAddonBinarySensor( + addon=addon, + coordinator=coordinator, + entity_description=entity_description, + ) + ) + + if coordinator.is_hass_os: + entities.append( + HassioOSBinarySensor( + coordinator=coordinator, + entity_description=entity_description, + ) + ) + async_add_entities(entities) class HassioAddonBinarySensor(HassioAddonEntity, BinarySensorEntity): """Binary sensor to track whether an update is available for a Hass.io add-on.""" - _attr_device_class = DEVICE_CLASS_UPDATE - @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return self.addon_info[self.attribute_name] + return self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug][ + self.entity_description.key + ] class HassioOSBinarySensor(HassioOSEntity, BinarySensorEntity): """Binary sensor to track whether an update is available for Hass.io OS.""" - _attr_device_class = DEVICE_CLASS_UPDATE - @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return self.os_info[self.attribute_name] + return self.coordinator.data[DATA_KEY_OS][self.entity_description.key] diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 6104e57fb17..134fba15f70 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -40,7 +40,6 @@ WS_TYPE_SUBSCRIBE = "supervisor/subscribe" EVENT_SUPERVISOR_EVENT = "supervisor_event" -# Add-on keys ATTR_VERSION = "version" ATTR_VERSION_LATEST = "version_latest" ATTR_UPDATE_AVAILABLE = "update_available" @@ -49,6 +48,10 @@ ATTR_URL = "url" ATTR_REPOSITORY = "repository" +DATA_KEY_ADDONS = "addons" +DATA_KEY_OS = "os" + + class SupervisorEntityModel(str, Enum): """Supervisor entity model.""" diff --git a/homeassistant/components/hassio/entity.py b/homeassistant/components/hassio/entity.py index 4885ba8979f..4a342e9965f 100644 --- a/homeassistant/components/hassio/entity.py +++ b/homeassistant/components/hassio/entity.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any from homeassistant.const import ATTR_NAME -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import DOMAIN, HassioDataUpdateCoordinator @@ -17,42 +17,16 @@ class HassioAddonEntity(CoordinatorEntity): def __init__( self, coordinator: HassioDataUpdateCoordinator, + entity_description: EntityDescription, addon: dict[str, Any], - attribute_name: str, - sensor_name: str, ) -> None: """Initialize base entity.""" - self.addon_slug = addon[ATTR_SLUG] - self.addon_name = addon[ATTR_NAME] - self._data_key = "addons" - self.attribute_name = attribute_name - self.sensor_name = sensor_name super().__init__(coordinator) - - @property - def addon_info(self) -> dict[str, Any]: - """Return add-on info.""" - return self.coordinator.data[self._data_key][self.addon_slug] - - @property - def name(self) -> str: - """Return entity name.""" - return f"{self.addon_name}: {self.sensor_name}" - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return False - - @property - def unique_id(self) -> str: - """Return unique ID for entity.""" - return f"{self.addon_slug}_{self.attribute_name}" - - @property - def device_info(self) -> DeviceInfo: - """Return device specific attributes.""" - return {"identifiers": {(DOMAIN, self.addon_slug)}} + self.entity_description = entity_description + self._addon_slug = addon[ATTR_SLUG] + self._attr_name = f"{addon[ATTR_NAME]}: {entity_description.name}" + self._attr_unique_id = f"{addon[ATTR_SLUG]}_{entity_description.key}" + self._attr_device_info = {"identifiers": {(DOMAIN, addon[ATTR_SLUG])}} class HassioOSEntity(CoordinatorEntity): @@ -61,36 +35,11 @@ class HassioOSEntity(CoordinatorEntity): def __init__( self, coordinator: HassioDataUpdateCoordinator, - attribute_name: str, - sensor_name: str, + entity_description: EntityDescription, ) -> None: """Initialize base entity.""" - self._data_key = "os" - self.attribute_name = attribute_name - self.sensor_name = sensor_name super().__init__(coordinator) - - @property - def os_info(self) -> dict[str, Any]: - """Return OS info.""" - return self.coordinator.data[self._data_key] - - @property - def name(self) -> str: - """Return entity name.""" - return f"Home Assistant Operating System: {self.sensor_name}" - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return False - - @property - def unique_id(self) -> str: - """Return unique ID for entity.""" - return f"home_assistant_os_{self.attribute_name}" - - @property - def device_info(self) -> DeviceInfo: - """Return device specific attributes.""" - return {"identifiers": {(DOMAIN, "OS")}} + self.entity_description = entity_description + self._attr_name = f"Home Assistant Operating System: {entity_description.name}" + self._attr_unique_id = f"home_assistant_os_{entity_description.key}" + self._attr_device_info = {"identifiers": {(DOMAIN, "OS")}} diff --git a/homeassistant/components/hassio/sensor.py b/homeassistant/components/hassio/sensor.py index c0c3e63715c..55678eb29c4 100644 --- a/homeassistant/components/hassio/sensor.py +++ b/homeassistant/components/hassio/sensor.py @@ -1,15 +1,28 @@ """Sensor platform for Hass.io addons.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity +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 ADDONS_COORDINATOR -from .const import ATTR_VERSION, ATTR_VERSION_LATEST +from .const import ATTR_VERSION, ATTR_VERSION_LATEST, DATA_KEY_ADDONS, DATA_KEY_OS from .entity import HassioAddonEntity, HassioOSEntity +ENTITY_DESCRIPTIONS = ( + SensorEntityDescription( + entity_registry_enabled_default=False, + key=ATTR_VERSION, + name="Version", + ), + SensorEntityDescription( + entity_registry_enabled_default=False, + key=ATTR_VERSION_LATEST, + name="Newest Version", + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -21,16 +34,23 @@ async def async_setup_entry( entities = [] - for attribute_name, sensor_name in ( - (ATTR_VERSION, "Version"), - (ATTR_VERSION_LATEST, "Newest Version"), - ): - for addon in coordinator.data["addons"].values(): + for entity_description in ENTITY_DESCRIPTIONS: + for addon in coordinator.data[DATA_KEY_ADDONS].values(): entities.append( - HassioAddonSensor(coordinator, addon, attribute_name, sensor_name) + HassioAddonSensor( + addon=addon, + coordinator=coordinator, + entity_description=entity_description, + ) ) + if coordinator.is_hass_os: - entities.append(HassioOSSensor(coordinator, attribute_name, sensor_name)) + entities.append( + HassioOSSensor( + coordinator=coordinator, + entity_description=entity_description, + ) + ) async_add_entities(entities) @@ -40,8 +60,10 @@ class HassioAddonSensor(HassioAddonEntity, SensorEntity): @property def native_value(self) -> str: - """Return state of entity.""" - return self.addon_info[self.attribute_name] + """Return native value of entity.""" + return self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug][ + self.entity_description.key + ] class HassioOSSensor(HassioOSEntity, SensorEntity): @@ -49,5 +71,5 @@ class HassioOSSensor(HassioOSEntity, SensorEntity): @property def native_value(self) -> str: - """Return state of entity.""" - return self.os_info[self.attribute_name] + """Return native value of entity.""" + return self.coordinator.data[DATA_KEY_OS][self.entity_description.key] From 013b998974c889c8a80d04637a9ea8e43c7e2fc3 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 17 Aug 2021 07:34:00 -0400 Subject: [PATCH 270/355] Relax zwave_js lock discovery rules to cover more use cases (#54710) --- .../components/zwave_js/discovery.py | 20 - tests/components/zwave_js/conftest.py | 20 +- tests/components/zwave_js/test_discovery.py | 11 + ...pp_electric_strike_lock_control_state.json | 568 ++++++++++++++++++ 4 files changed, 598 insertions(+), 21 deletions(-) create mode 100644 tests/fixtures/zwave_js/lock_popp_electric_strike_lock_control_state.json diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 58dae39781e..dcae65b0395 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -359,24 +359,11 @@ DISCOVERY_SCHEMAS = [ get_config_parameter_discovery_schema( property_name={"Door lock mode"}, device_class_generic={"Entry Control"}, - device_class_specific={ - "Door Lock", - "Advanced Door Lock", - "Secure Keypad Door Lock", - "Secure Lockbox", - }, ), # ====== START OF GENERIC MAPPING SCHEMAS ======= # locks ZWaveDiscoverySchema( platform="lock", - device_class_generic={"Entry Control"}, - device_class_specific={ - "Door Lock", - "Advanced Door Lock", - "Secure Keypad Door Lock", - "Secure Lockbox", - }, primary_value=ZWaveValueDiscoverySchema( command_class={ CommandClass.LOCK, @@ -390,13 +377,6 @@ DISCOVERY_SCHEMAS = [ ZWaveDiscoverySchema( platform="binary_sensor", hint="property", - device_class_generic={"Entry Control"}, - device_class_specific={ - "Door Lock", - "Advanced Door Lock", - "Secure Keypad Door Lock", - "Secure Lockbox", - }, primary_value=ZWaveValueDiscoverySchema( command_class={ CommandClass.LOCK, diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 75b5ab65d38..8165dac33a7 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -452,6 +452,14 @@ def aeotec_zw164_siren_state_fixture(): return json.loads(load_fixture("zwave_js/aeotec_zw164_siren_state.json")) +@pytest.fixture(name="lock_popp_electric_strike_lock_control_state", scope="session") +def lock_popp_electric_strike_lock_control_state_fixture(): + """Load the popp electric strike lock control node state fixture data.""" + return json.loads( + load_fixture("zwave_js/lock_popp_electric_strike_lock_control_state.json") + ) + + @pytest.fixture(name="client") def mock_client_fixture(controller_state, version_state, log_config_state): """Mock a client.""" @@ -830,12 +838,22 @@ def ge_in_wall_dimmer_switch_fixture(client, ge_in_wall_dimmer_switch_state): @pytest.fixture(name="aeotec_zw164_siren") def aeotec_zw164_siren_fixture(client, aeotec_zw164_siren_state): - """Mock a wallmote central scene node.""" + """Mock a aeotec zw164 siren node.""" node = Node(client, copy.deepcopy(aeotec_zw164_siren_state)) client.driver.controller.nodes[node.node_id] = node return node +@pytest.fixture(name="lock_popp_electric_strike_lock_control") +def lock_popp_electric_strike_lock_control_fixture( + client, lock_popp_electric_strike_lock_control_state +): + """Mock a popp electric strike lock control node.""" + node = Node(client, copy.deepcopy(lock_popp_electric_strike_lock_control_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/test_discovery.py b/tests/components/zwave_js/test_discovery.py index 8914019cd43..9758d3b0f44 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -57,6 +57,17 @@ async def test_vision_security_zl7432( assert state.attributes["assumed_state"] +async def test_lock_popp_electric_strike_lock_control( + hass, client, lock_popp_electric_strike_lock_control, integration +): + """Test that the Popp Electric Strike Lock Control gets discovered correctly.""" + assert hass.states.get("lock.node_62") is not None + assert ( + hass.states.get("binary_sensor.node_62_the_current_status_of_the_door") + is not None + ) + + async def test_firmware_version_range_exception(hass): """Test FirmwareVersionRange exception.""" with pytest.raises(ValueError): diff --git a/tests/fixtures/zwave_js/lock_popp_electric_strike_lock_control_state.json b/tests/fixtures/zwave_js/lock_popp_electric_strike_lock_control_state.json new file mode 100644 index 00000000000..2b4a3a88984 --- /dev/null +++ b/tests/fixtures/zwave_js/lock_popp_electric_strike_lock_control_state.json @@ -0,0 +1,568 @@ +{ + "nodeId": 62, + "index": 0, + "installerIcon": 768, + "userIcon": 768, + "status": 4, + "ready": true, + "isListening": false, + "isRouting": true, + "isSecure": true, + "manufacturerId": 340, + "productId": 1, + "productType": 5, + "firmwareVersion": "1.3", + "zwavePlusVersion": 1, + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 62, + "index": 0, + "installerIcon": 768, + "userIcon": 768, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 64, + "label": "Entry Control" + }, + "specific": { + "key": 10, + "label": "Lockbox" + }, + "mandatorySupportedCCs": [113, 133, 98, 114, 152, 134], + "mandatoryControlledCCs": [] + } + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 48, + "commandClassName": "Binary Sensor", + "property": "Door/Window", + "propertyName": "Door/Window", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Door/Window", + "ccSpecific": { + "sensorType": 10 + } + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "currentMode", + "propertyName": "currentMode", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current lock mode", + "min": 0, + "max": 255, + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "targetMode", + "propertyName": "targetMode", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target lock mode", + "min": 0, + "max": 255, + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } + } + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "outsideHandlesCanOpenDoor", + "propertyName": "outsideHandlesCanOpenDoor", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which outside handles can open the door (actual status)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "insideHandlesCanOpenDoor", + "propertyName": "insideHandlesCanOpenDoor", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which inside handles can open the door (actual status)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "latchStatus", + "propertyName": "latchStatus", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the latch" + }, + "value": "closed" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "boltStatus", + "propertyName": "boltStatus", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the bolt" + }, + "value": "unlocked" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "doorStatus", + "propertyName": "doorStatus", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the door" + }, + "value": "closed" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "lockTimeout", + "propertyName": "lockTimeout", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Seconds until lock mode times out" + } + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "operationType", + "propertyName": "operationType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Lock operation type", + "min": 0, + "max": 255, + "states": { + "1": "Constant", + "2": "Timed" + } + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "outsideHandlesCanOpenDoorConfiguration", + "propertyName": "outsideHandlesCanOpenDoorConfiguration", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which outside handles can open the door (configuration)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "insideHandlesCanOpenDoorConfiguration", + "propertyName": "insideHandlesCanOpenDoorConfiguration", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which inside handles can open the door (configuration)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "lockTimeoutConfiguration", + "propertyName": "lockTimeoutConfiguration", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Duration of timed mode in seconds", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Access Control", + "propertyKey": "Door state", + "propertyName": "Access Control", + "propertyKeyName": "Door state", + "ccVersion": 5, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Door state", + "ccSpecific": { + "notificationType": 6 + }, + "min": 0, + "max": 255, + "states": { + "22": "Window/door is open", + "23": "Window/door is closed" + } + }, + "value": 23 + }, + { + "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": 340 + }, + { + "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": 5 + }, + { + "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": 1 + }, + { + "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": 100 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "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": ["1.3"] + }, + { + "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 + } + ], + "isFrequentListening": "1000ms", + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 7, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 64, + "label": "Entry Control" + }, + "specific": { + "key": 10, + "label": "Lockbox" + }, + "mandatorySupportedCCs": [113, 133, 98, 114, 152, 134], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 48, + "name": "Binary Sensor", + "version": 2, + "isSecure": true + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": true + }, + { + "id": 98, + "name": "Door Lock", + "version": 2, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 5, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": true + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 3, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": true + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ], + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0154:0x0005:0x0001:1.3", + "statistics": { + "commandsTX": 1, + "commandsRX": 0, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 + } +} From 9dab920d01c831659b9fe71ab16cf41122cb99e8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 Aug 2021 14:26:02 +0200 Subject: [PATCH 271/355] DSMR: Remove icon from sensors with gas device class (#54752) --- homeassistant/components/dsmr/const.py | 3 --- tests/components/dsmr/test_sensor.py | 5 ----- 2 files changed, 8 deletions(-) diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index 0043113772e..6c392526ee3 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -251,7 +251,6 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( dsmr_versions={"4", "5", "5L"}, is_gas=True, force_update=True, - icon="mdi:fire", device_class=DEVICE_CLASS_GAS, state_class=STATE_CLASS_TOTAL_INCREASING, ), @@ -261,7 +260,6 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( dsmr_versions={"5B"}, is_gas=True, force_update=True, - icon="mdi:fire", device_class=DEVICE_CLASS_GAS, state_class=STATE_CLASS_TOTAL_INCREASING, ), @@ -271,7 +269,6 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( dsmr_versions={"2.2"}, is_gas=True, force_update=True, - icon="mdi:fire", device_class=DEVICE_CLASS_GAS, state_class=STATE_CLASS_TOTAL_INCREASING, ), diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 0f1c55f47b6..88e984cea1b 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -167,7 +167,6 @@ async def test_default_setup(hass, dsmr_connection_fixture): gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS - assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" assert gas_consumption.attributes.get(ATTR_LAST_RESET) is None assert ( gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING @@ -269,7 +268,6 @@ async def test_v4_meter(hass, dsmr_connection_fixture): assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS - assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" assert gas_consumption.attributes.get(ATTR_LAST_RESET) is None assert ( gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING @@ -341,7 +339,6 @@ async def test_v5_meter(hass, dsmr_connection_fixture): gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS - assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" assert gas_consumption.attributes.get(ATTR_LAST_RESET) is None assert ( gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING @@ -424,7 +421,6 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture): gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS - assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" assert gas_consumption.attributes.get(ATTR_LAST_RESET) is None assert ( gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING @@ -496,7 +492,6 @@ async def test_belgian_meter(hass, dsmr_connection_fixture): gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is DEVICE_CLASS_GAS - assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" assert gas_consumption.attributes.get(ATTR_LAST_RESET) is None assert ( gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING From 346310ccafed1f4dfa60237b64723f7fcb0423cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Tue, 17 Aug 2021 14:59:56 +0200 Subject: [PATCH 272/355] Bump pyfronius version to 0.5.5 (#54758) - allows for trailing slashes in configuration (which would otherwise cause errors in the newest fronius firmware) - fixes units of energy related sensors --- homeassistant/components/fronius/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fronius/manifest.json b/homeassistant/components/fronius/manifest.json index 1ae95d30fd5..a8e9c44805d 100644 --- a/homeassistant/components/fronius/manifest.json +++ b/homeassistant/components/fronius/manifest.json @@ -2,7 +2,7 @@ "domain": "fronius", "name": "Fronius", "documentation": "https://www.home-assistant.io/integrations/fronius", - "requirements": ["pyfronius==0.5.3"], + "requirements": ["pyfronius==0.5.5"], "codeowners": ["@nielstron"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index dfbfd346929..21d8d158545 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1466,7 +1466,7 @@ pyfreedompro==1.1.0 pyfritzhome==0.6.2 # homeassistant.components.fronius -pyfronius==0.5.3 +pyfronius==0.5.5 # homeassistant.components.ifttt pyfttt==0.3 From 043841e70f7fabdd9dd1bee94a9a19fb5ed03d90 Mon Sep 17 00:00:00 2001 From: Robin Wohlers-Reichel Date: Tue, 17 Aug 2021 23:06:22 +1000 Subject: [PATCH 273/355] Solax 0.2.8 (#54759) --- 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 d14cfea2501..f6a6f581e12 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.6"], + "requirements": ["solax==0.2.8"], "codeowners": ["@squishykid"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 21d8d158545..1f0e4e1b665 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2165,7 +2165,7 @@ solaredge-local==0.2.0 solaredge==0.0.2 # homeassistant.components.solax -solax==0.2.6 +solax==0.2.8 # homeassistant.components.honeywell somecomfort==0.5.2 From f39dc749bb81db0c271eac508fad265b37631bb1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 Aug 2021 16:28:18 +0200 Subject: [PATCH 274/355] Toon: Remove icon from sensors with gas device class (#54753) --- homeassistant/components/toon/const.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/toon/const.py b/homeassistant/components/toon/const.py index 1c58ec2cde7..678b3400b88 100644 --- a/homeassistant/components/toon/const.py +++ b/homeassistant/components/toon/const.py @@ -127,7 +127,6 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "day_average", ATTR_DEVICE_CLASS: DEVICE_CLASS_GAS, ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, - ATTR_ICON: "mdi:gas-cylinder", ATTR_DEFAULT_ENABLED: False, }, "gas_daily_usage": { @@ -136,7 +135,6 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "day_usage", ATTR_DEVICE_CLASS: DEVICE_CLASS_GAS, ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, - ATTR_ICON: "mdi:gas-cylinder", }, "gas_daily_cost": { ATTR_NAME: "Gas Cost Today", @@ -150,7 +148,6 @@ SENSOR_ENTITIES = { ATTR_SECTION: "gas_usage", ATTR_MEASUREMENT: "meter", ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, - ATTR_ICON: "mdi:gas-cylinder", ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, ATTR_DEVICE_CLASS: DEVICE_CLASS_GAS, ATTR_DEFAULT_ENABLED: False, From ea8061469c25e5e90abfd98931048e2e702b99ad Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 17 Aug 2021 15:29:52 +0100 Subject: [PATCH 275/355] Deprecate homekit_controller's air quality entity in favor of separate sensor entities (#54673) --- .../homekit_controller/air_quality.py | 18 + .../components/homekit_controller/sensor.py | 55 +- .../specific_devices/test_arlo_baby.py | 84 +++ .../homekit_controller/test_air_quality.py | 8 + .../homekit_controller/arlo_baby.json | 484 ++++++++++++++++++ 5 files changed, 647 insertions(+), 2 deletions(-) create mode 100644 tests/components/homekit_controller/specific_devices/test_arlo_baby.py create mode 100644 tests/fixtures/homekit_controller/arlo_baby.json diff --git a/homeassistant/components/homekit_controller/air_quality.py b/homeassistant/components/homekit_controller/air_quality.py index 2a162eb2b2a..b4ca2f4918a 100644 --- a/homeassistant/components/homekit_controller/air_quality.py +++ b/homeassistant/components/homekit_controller/air_quality.py @@ -1,4 +1,6 @@ """Support for HomeKit Controller air quality sensors.""" +import logging + from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes @@ -7,6 +9,8 @@ from homeassistant.core import callback from . import KNOWN_DEVICES, HomeKitEntity +_LOGGER = logging.getLogger(__name__) + AIR_QUALITY_TEXT = { 0: "unknown", 1: "excellent", @@ -20,6 +24,20 @@ AIR_QUALITY_TEXT = { class HomeAirQualitySensor(HomeKitEntity, AirQualityEntity): """Representation of a HomeKit Controller Air Quality sensor.""" + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + _LOGGER.warning( + "The homekit_controller air_quality entity has been " + "deprecated and will be removed in 2021.12.0" + ) + await super().async_added_to_hass() + + @property + def entity_registry_enabled_default(self) -> bool: + """Whether or not to enable this entity by default.""" + # This entity is deprecated, so don't enable by default + return False + def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" return [ diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index b599e7263c8..ac4f19dadb4 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -4,12 +4,19 @@ from aiohomekit.model.services import ServicesTypes from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity 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, LIGHT_LUX, PERCENTAGE, @@ -52,7 +59,7 @@ SIMPLE_SENSOR = { "state_class": STATE_CLASS_MEASUREMENT, "unit": PRESSURE_HPA, }, - CharacteristicsTypes.get_uuid(CharacteristicsTypes.TEMPERATURE_CURRENT): { + CharacteristicsTypes.TEMPERATURE_CURRENT: { "name": "Current Temperature", "device_class": DEVICE_CLASS_TEMPERATURE, "state_class": STATE_CLASS_MEASUREMENT, @@ -62,7 +69,7 @@ SIMPLE_SENSOR = { "probe": lambda char: char.service.type != ServicesTypes.get_uuid(ServicesTypes.TEMPERATURE_SENSOR), }, - CharacteristicsTypes.get_uuid(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT): { + CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT: { "name": "Current Humidity", "device_class": DEVICE_CLASS_HUMIDITY, "state_class": STATE_CLASS_MEASUREMENT, @@ -72,8 +79,52 @@ SIMPLE_SENSOR = { "probe": lambda char: char.service.type != ServicesTypes.get_uuid(ServicesTypes.HUMIDITY_SENSOR), }, + CharacteristicsTypes.AIR_QUALITY: { + "name": "Air Quality", + "device_class": DEVICE_CLASS_AQI, + "state_class": STATE_CLASS_MEASUREMENT, + }, + CharacteristicsTypes.DENSITY_PM25: { + "name": "PM2.5 Density", + "device_class": DEVICE_CLASS_PM25, + "state_class": STATE_CLASS_MEASUREMENT, + "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + }, + CharacteristicsTypes.DENSITY_PM10: { + "name": "PM10 Density", + "device_class": DEVICE_CLASS_PM10, + "state_class": STATE_CLASS_MEASUREMENT, + "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + }, + CharacteristicsTypes.DENSITY_OZONE: { + "name": "Ozone Density", + "device_class": DEVICE_CLASS_OZONE, + "state_class": STATE_CLASS_MEASUREMENT, + "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + }, + CharacteristicsTypes.DENSITY_NO2: { + "name": "Nitrogen Dioxide Density", + "device_class": DEVICE_CLASS_NITROGEN_DIOXIDE, + "state_class": STATE_CLASS_MEASUREMENT, + "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + }, + CharacteristicsTypes.DENSITY_SO2: { + "name": "Sulphur Dioxide Density", + "device_class": DEVICE_CLASS_SULPHUR_DIOXIDE, + "state_class": STATE_CLASS_MEASUREMENT, + "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + }, } +# For legacy reasons, "built-in" characteristic types are in their short form +# And vendor types don't have a short form +# This means long and short forms get mixed up in this dict, and comparisons +# don't work! +# We call get_uuid on *every* type to normalise them to the long form +# Eventually aiohomekit will use the long form exclusively amd this can be removed. +for k, v in list(SIMPLE_SENSOR.items()): + SIMPLE_SENSOR[CharacteristicsTypes.get_uuid(k)] = SIMPLE_SENSOR.pop(k) + class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): """Representation of a Homekit humidity sensor.""" diff --git a/tests/components/homekit_controller/specific_devices/test_arlo_baby.py b/tests/components/homekit_controller/specific_devices/test_arlo_baby.py new file mode 100644 index 00000000000..86fb9f65f11 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_arlo_baby.py @@ -0,0 +1,84 @@ +"""Make sure that an Arlo Baby can be setup.""" + +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_arlo_baby_setup(hass): + """Test that an Arlo Baby can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "arlo_baby.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + sensors = [ + ( + "camera.arlobabya0", + "homekit-00A0000000000-aid:1", + "ArloBabyA0", + ), + ( + "binary_sensor.arlobabya0", + "homekit-00A0000000000-500", + "ArloBabyA0", + ), + ( + "sensor.arlobabya0_battery", + "homekit-00A0000000000-700", + "ArloBabyA0 Battery", + ), + ( + "sensor.arlobabya0_humidity", + "homekit-00A0000000000-900", + "ArloBabyA0 Humidity", + ), + ( + "sensor.arlobabya0_temperature", + "homekit-00A0000000000-1000", + "ArloBabyA0 Temperature", + ), + ( + "sensor.arlobabya0_air_quality", + "homekit-00A0000000000-aid:1-sid:800-cid:802", + "ArloBabyA0 - Air Quality", + ), + ( + "light.arlobabya0", + "homekit-00A0000000000-1100", + "ArloBabyA0", + ), + ] + + device_ids = set() + + for (entity_id, unique_id, friendly_name) in sensors: + 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 == "Netgear, Inc" + assert device.name == "ArloBabyA0" + assert device.model == "ABC1000" + assert device.sw_version == "1.10.931" + 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/test_air_quality.py b/tests/components/homekit_controller/test_air_quality.py index 52c79f2b28a..f75335ca357 100644 --- a/tests/components/homekit_controller/test_air_quality.py +++ b/tests/components/homekit_controller/test_air_quality.py @@ -2,6 +2,8 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes +from homeassistant.helpers import entity_registry as er + from tests.components.homekit_controller.common import setup_test_component @@ -35,6 +37,12 @@ async def test_air_quality_sensor_read_state(hass, utcnow): """Test reading the state of a HomeKit temperature sensor accessory.""" helper = await setup_test_component(hass, create_air_quality_sensor_service) + entity_registry = er.async_get(hass) + entity_registry.async_update_entity( + entity_id="air_quality.testdevice", disabled_by=None + ) + await hass.async_block_till_done() + state = await helper.poll_and_get_state() assert state.state == "4444" diff --git a/tests/fixtures/homekit_controller/arlo_baby.json b/tests/fixtures/homekit_controller/arlo_baby.json new file mode 100644 index 00000000000..6a124a5f56f --- /dev/null +++ b/tests/fixtures/homekit_controller/arlo_baby.json @@ -0,0 +1,484 @@ +[ + { + "aid": 1, + "services": [ + { + "type": "0000003E-0000-1000-8000-0026BB765291", + "iid": 1, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "value": "ArloBabyA0", + "perms": [ + "pr" + ], + "format": "string" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "value": "Netgear, Inc", + "perms": [ + "pr" + ], + "format": "string" + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 4, + "value": "00A0000000000", + "perms": [ + "pr" + ], + "format": "string" + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "value": "ABC1000", + "perms": [ + "pr" + ], + "format": "string" + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 7, + "value": "1.10.931", + "perms": [ + "pr" + ], + "format": "string" + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": [ + "pw" + ], + "format": "bool" + } + ] + }, + { + "type": "000000A2-0000-1000-8000-0026BB765291", + "iid": 20, + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 21, + "value": "1.1.0", + "perms": [ + "pr" + ], + "format": "string" + } + ] + }, + { + "type": "00000110-0000-1000-8000-0026BB765291", + "iid": 100, + "characteristics": [ + { + "type": "00000120-0000-1000-8000-0026BB765291", + "iid": 106, + "value": "AQEB", + "perms": [ + "pr", + "ev" + ], + "format": "tlv8" + }, + { + "type": "00000114-0000-1000-8000-0026BB765291", + "iid": 101, + "value": "AY8BAQACFQEBAAEBAQEBAQIBAAMBAAQBAAUBAQMLAQKABwICOAQDAR4DCwECAAUCAsADAwEeAwsBAgAEAgIAAwMBHgMLAQIABQIC0AIDAR4DCwECgAICAmgBAwEeAwsBAuABAgIOAQMBHgMLAQKAAgIC4AEDAR4DCwEC4AECAmgBAwEeAwsBAkABAgLwAAMBHg==", + "perms": [ + "pr" + ], + "format": "tlv8" + }, + { + "type": "00000115-0000-1000-8000-0026BB765291", + "iid": 102, + "value": "AQ4BAQMCCQEBAQIBAAMBAQIBAA==", + "perms": [ + "pr" + ], + "format": "tlv8" + }, + { + "type": "00000116-0000-1000-8000-0026BB765291", + "iid": 103, + "value": "AgEAAgEBAgEC", + "perms": [ + "pr" + ], + "format": "tlv8" + }, + { + "type": "00000117-0000-1000-8000-0026BB765291", + "iid": 104, + "value": "", + "perms": [ + "pr", + "pw" + ], + "format": "tlv8" + }, + { + "type": "00000118-0000-1000-8000-0026BB765291", + "iid": 108, + "value": "", + "perms": [ + "pr", + "pw" + ], + "format": "tlv8" + } + ] + }, + { + "type": "00000110-0000-1000-8000-0026BB765291", + "iid": 110, + "characteristics": [ + { + "type": "00000120-0000-1000-8000-0026BB765291", + "iid": 116, + "value": "AQEA", + "perms": [ + "pr", + "ev" + ], + "format": "tlv8" + }, + { + "type": "00000114-0000-1000-8000-0026BB765291", + "iid": 111, + "value": "AWgBAQACFQEBAAEBAQEBAQIBAAMBAAQBAAUBAQMLAQIABQIC0AIDAR4DCwECgAICAmgBAwEeAwsBAuABAgIOAQMBHgMLAQKAAgIC4AEDAR4DCwEC4AECAmgBAwEeAwsBAkABAgLwAAMBHg==", + "perms": [ + "pr" + ], + "format": "tlv8" + }, + { + "type": "00000115-0000-1000-8000-0026BB765291", + "iid": 112, + "value": "AQ4BAQMCCQEBAQIBAAMBAQIBAA==", + "perms": [ + "pr" + ], + "format": "tlv8" + }, + { + "type": "00000116-0000-1000-8000-0026BB765291", + "iid": 113, + "value": "AgEAAgEBAgEC", + "perms": [ + "pr" + ], + "format": "tlv8" + }, + { + "type": "00000117-0000-1000-8000-0026BB765291", + "iid": 114, + "value": "", + "perms": [ + "pr", + "pw" + ], + "format": "tlv8" + }, + { + "type": "00000118-0000-1000-8000-0026BB765291", + "iid": 118, + "value": "", + "perms": [ + "pr", + "pw" + ], + "format": "tlv8" + } + ] + }, + { + "type": "00000112-0000-1000-8000-0026BB765291", + "iid": 300, + "characteristics": [ + { + "type": "0000011A-0000-1000-8000-0026BB765291", + "iid": 302, + "value": false, + "perms": [ + "pr", + "pw", + "ev" + ], + "format": "bool" + } + ] + }, + { + "type": "00000113-0000-1000-8000-0026BB765291", + "iid": 400, + "characteristics": [ + { + "type": "0000011A-0000-1000-8000-0026BB765291", + "iid": 402, + "value": false, + "perms": [ + "pr", + "pw", + "ev" + ], + "format": "bool" + }, + { + "type": "00000119-0000-1000-8000-0026BB765291", + "iid": 403, + "value": 50, + "perms": [ + "pr", + "pw", + "ev" + ], + "format": "uint8", + "minValue": 0, + "maxValue": 100, + "minStep": 1, + "unit": "percentage" + } + ] + }, + { + "type": "00000085-0000-1000-8000-0026BB765291", + "iid": 500, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 501, + "value": "Motion", + "perms": [ + "pr" + ], + "format": "string" + }, + { + "type": "00000022-0000-1000-8000-0026BB765291", + "iid": 502, + "value": false, + "perms": [ + "pr", + "ev" + ], + "format": "bool" + } + ] + }, + { + "type": "00000096-0000-1000-8000-0026BB765291", + "iid": 700, + "characteristics": [ + { + "type": "00000068-0000-1000-8000-0026BB765291", + "iid": 701, + "value": 82, + "perms": [ + "pr", + "ev" + ], + "format": "uint8", + "minValue": 0, + "maxValue": 100, + "minStep": 1, + "unit": "percentage" + }, + { + "type": "0000008F-0000-1000-8000-0026BB765291", + "iid": 702, + "value": 0, + "perms": [ + "pr", + "ev" + ], + "format": "uint8", + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "type": "00000079-0000-1000-8000-0026BB765291", + "iid": 703, + "value": 0, + "perms": [ + "pr", + "ev" + ], + "format": "uint8", + "minValue": 0, + "maxValue": 2, + "minStep": 1 + } + ] + }, + { + "type": "0000008D-0000-1000-8000-0026BB765291", + "iid": 800, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 801, + "value": "Air Quality", + "perms": [ + "pr" + ], + "format": "string" + }, + { + "type": "00000095-0000-1000-8000-0026BB765291", + "iid": 802, + "value": 1, + "perms": [ + "pr", + "ev" + ], + "format": "uint8", + "minValue": 0, + "maxValue": 5, + "minStep": 1 + } + ] + }, + { + "type": "00000082-0000-1000-8000-0026BB765291", + "iid": 900, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 901, + "value": "Humidity", + "perms": [ + "pr" + ], + "format": "string" + }, + { + "type": "00000010-0000-1000-8000-0026BB765291", + "iid": 902, + "value": 60.099998, + "perms": [ + "pr", + "ev" + ], + "format": "float", + "minValue": 0.0, + "maxValue": 100.0, + "minStep": 1.0, + "unit": "percentage" + } + ] + }, + { + "type": "0000008A-0000-1000-8000-0026BB765291", + "iid": 1000, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 1001, + "value": "Temperature", + "perms": [ + "pr" + ], + "format": "string" + }, + { + "type": "00000011-0000-1000-8000-0026BB765291", + "iid": 1002, + "value": 24.0, + "perms": [ + "pr", + "ev" + ], + "format": "float", + "minValue": 0.0, + "maxValue": 100.0, + "minStep": 0.1, + "unit": "celsius" + } + ] + }, + { + "type": "00000043-0000-1000-8000-0026BB765291", + "iid": 1100, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 1101, + "value": "Nightlight", + "perms": [ + "pr" + ], + "format": "string" + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 1102, + "value": false, + "perms": [ + "pr", + "pw", + "ev" + ], + "format": "bool" + }, + { + "type": "00000008-0000-1000-8000-0026BB765291", + "iid": 1103, + "value": 100, + "perms": [ + "pr", + "pw", + "ev" + ], + "format": "int", + "minValue": 0, + "maxValue": 100, + "minStep": 1, + "unit": "percentage" + }, + { + "type": "00000013-0000-1000-8000-0026BB765291", + "iid": 1104, + "value": 0.0, + "perms": [ + "pr", + "pw", + "ev" + ], + "format": "float", + "minValue": 0.0, + "maxValue": 360.0, + "minStep": 1.0, + "unit": "arcdegrees" + }, + { + "type": "0000002F-0000-1000-8000-0026BB765291", + "iid": 1105, + "value": 0.0, + "perms": [ + "pr", + "pw", + "ev" + ], + "format": "float", + "minValue": 0.0, + "maxValue": 100.0, + "minStep": 1.0, + "unit": "percentage" + } + ] + } + ] + } +] \ No newline at end of file From 35f563e23e3c6a83c4db27ef1ee83201d5c72c20 Mon Sep 17 00:00:00 2001 From: LonePurpleWolf <38847877+LonePurpleWolf@users.noreply.github.com> Date: Wed, 18 Aug 2021 01:29:20 +1000 Subject: [PATCH 276/355] Airtouch4 integration (#43513) * airtouch 4 climate control integration * enhance tests for airtouch. Fix linting issues * Fix tests * rework tests * fix latest qa issues * Clean up * add already_configured message * Use common string * further qa fixes * simplify airtouch4 domain storage Co-authored-by: Franck Nijhof Co-authored-by: Martin Hjelmare --- .coveragerc | 3 + CODEOWNERS | 1 + .../components/airtouch4/__init__.py | 81 +++++ homeassistant/components/airtouch4/climate.py | 335 ++++++++++++++++++ .../components/airtouch4/config_flow.py | 50 +++ homeassistant/components/airtouch4/const.py | 3 + .../components/airtouch4/manifest.json | 13 + .../components/airtouch4/strings.json | 19 + .../components/airtouch4/translations/en.json | 17 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/airtouch4/__init__.py | 1 + .../components/airtouch4/test_config_flow.py | 123 +++++++ 14 files changed, 653 insertions(+) create mode 100644 homeassistant/components/airtouch4/__init__.py create mode 100644 homeassistant/components/airtouch4/climate.py create mode 100644 homeassistant/components/airtouch4/config_flow.py create mode 100644 homeassistant/components/airtouch4/const.py create mode 100644 homeassistant/components/airtouch4/manifest.json create mode 100644 homeassistant/components/airtouch4/strings.json create mode 100644 homeassistant/components/airtouch4/translations/en.json create mode 100644 tests/components/airtouch4/__init__.py create mode 100644 tests/components/airtouch4/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index e25f664efe8..fd4f87da858 100644 --- a/.coveragerc +++ b/.coveragerc @@ -36,6 +36,9 @@ omit = homeassistant/components/agent_dvr/helpers.py homeassistant/components/airnow/__init__.py homeassistant/components/airnow/sensor.py + homeassistant/components/airtouch4/__init__.py + homeassistant/components/airtouch4/climate.py + homeassistant/components/airtouch4/const.py homeassistant/components/airvisual/__init__.py homeassistant/components/airvisual/sensor.py homeassistant/components/aladdin_connect/* diff --git a/CODEOWNERS b/CODEOWNERS index c6696c485fe..1dedb1d421b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -29,6 +29,7 @@ homeassistant/components/aemet/* @noltari homeassistant/components/agent_dvr/* @ispysoftware homeassistant/components/airly/* @bieniu homeassistant/components/airnow/* @asymworks +homeassistant/components/airtouch4/* @LonePurpleWolf homeassistant/components/airvisual/* @bachya homeassistant/components/alarmdecoder/* @ajschmidt8 homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy diff --git a/homeassistant/components/airtouch4/__init__.py b/homeassistant/components/airtouch4/__init__.py new file mode 100644 index 00000000000..0ec63161ea3 --- /dev/null +++ b/homeassistant/components/airtouch4/__init__.py @@ -0,0 +1,81 @@ +"""The AirTouch4 integration.""" +import logging + +from airtouch4pyapi import AirTouch +from airtouch4pyapi.airtouch import AirTouchStatus + +from homeassistant.components.climate import SCAN_INTERVAL +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.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["climate"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up AirTouch4 from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + host = entry.data[CONF_HOST] + airtouch = AirTouch(host) + await airtouch.UpdateInfo() + info = airtouch.GetAcs() + if not info: + raise ConfigEntryNotReady + coordinator = AirtouchDataUpdateCoordinator(hass, airtouch) + await coordinator.async_config_entry_first_refresh() + hass.data[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 a config entry.""" + 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 AirtouchDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Airtouch data.""" + + def __init__(self, hass, airtouch): + """Initialize global Airtouch data updater.""" + self.airtouch = airtouch + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + + async def _async_update_data(self): + """Fetch data from Airtouch.""" + await self.airtouch.UpdateInfo() + if self.airtouch.Status != AirTouchStatus.OK: + raise UpdateFailed("Airtouch connection issue") + return { + "acs": [ + {"ac_number": ac.AcNumber, "is_on": ac.IsOn} + for ac in self.airtouch.GetAcs() + ], + "groups": [ + { + "group_number": group.GroupNumber, + "group_name": group.GroupName, + "is_on": group.IsOn, + } + for group in self.airtouch.GetGroups() + ], + } diff --git a/homeassistant/components/airtouch4/climate.py b/homeassistant/components/airtouch4/climate.py new file mode 100644 index 00000000000..7202feb0527 --- /dev/null +++ b/homeassistant/components/airtouch4/climate.py @@ -0,0 +1,335 @@ +"""AirTouch 4 component to control of AirTouch 4 Climate Devices.""" + +import logging + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + FAN_AUTO, + FAN_DIFFUSE, + FAN_FOCUS, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SUPPORT_FAN_MODE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.core import callback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN + +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE +AT_TO_HA_STATE = { + "Heat": HVAC_MODE_HEAT, + "Cool": HVAC_MODE_COOL, + "AutoHeat": HVAC_MODE_AUTO, # airtouch reports either autoheat or autocool + "AutoCool": HVAC_MODE_AUTO, + "Auto": HVAC_MODE_AUTO, + "Dry": HVAC_MODE_DRY, + "Fan": HVAC_MODE_FAN_ONLY, +} + +HA_STATE_TO_AT = { + HVAC_MODE_HEAT: "Heat", + HVAC_MODE_COOL: "Cool", + HVAC_MODE_AUTO: "Auto", + HVAC_MODE_DRY: "Dry", + HVAC_MODE_FAN_ONLY: "Fan", + HVAC_MODE_OFF: "Off", +} + +AT_TO_HA_FAN_SPEED = { + "Quiet": FAN_DIFFUSE, + "Low": FAN_LOW, + "Medium": FAN_MEDIUM, + "High": FAN_HIGH, + "Powerful": FAN_FOCUS, + "Auto": FAN_AUTO, + "Turbo": "turbo", +} + +AT_GROUP_MODES = [HVAC_MODE_OFF, HVAC_MODE_FAN_ONLY] + +HA_FAN_SPEED_TO_AT = {value: key for key, value in AT_TO_HA_FAN_SPEED.items()} + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Airtouch 4.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + info = coordinator.data + entities = [ + AirtouchGroup(coordinator, group["group_number"], info) + for group in info["groups"] + ] + [AirtouchAC(coordinator, ac["ac_number"], info) for ac in info["acs"]] + + _LOGGER.debug(" Found entities %s", entities) + + async_add_entities(entities) + + +class AirtouchAC(CoordinatorEntity, ClimateEntity): + """Representation of an AirTouch 4 ac.""" + + _attr_supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE + _attr_temperature_unit = TEMP_CELSIUS + + def __init__(self, coordinator, ac_number, info): + """Initialize the climate device.""" + super().__init__(coordinator) + self._ac_number = ac_number + self._airtouch = coordinator.airtouch + self._info = info + self._unit = self._airtouch.GetAcs()[self._ac_number] + + @callback + def _handle_coordinator_update(self): + self._unit = self._airtouch.GetAcs()[self._ac_number] + return super()._handle_coordinator_update() + + @property + def device_info(self): + """Return device info for this device.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Airtouch", + "model": "Airtouch 4", + } + + @property + def unique_id(self): + """Return unique ID for this device.""" + return f"ac_{self._ac_number}" + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._unit.Temperature + + @property + def name(self): + """Return the name of the climate device.""" + return f"AC {self._ac_number}" + + @property + def fan_mode(self): + """Return fan mode of the AC this group belongs to.""" + return AT_TO_HA_FAN_SPEED[self._airtouch.acs[self._ac_number].AcFanSpeed] + + @property + def fan_modes(self): + """Return the list of available fan modes.""" + airtouch_fan_speeds = self._airtouch.GetSupportedFanSpeedsForAc(self._ac_number) + return [AT_TO_HA_FAN_SPEED[speed] for speed in airtouch_fan_speeds] + + @property + def hvac_mode(self): + """Return hvac target hvac state.""" + is_off = self._unit.PowerState == "Off" + if is_off: + return HVAC_MODE_OFF + + return AT_TO_HA_STATE[self._airtouch.acs[self._ac_number].AcMode] + + @property + def hvac_modes(self): + """Return the list of available operation modes.""" + airtouch_modes = self._airtouch.GetSupportedCoolingModesForAc(self._ac_number) + modes = [AT_TO_HA_STATE[mode] for mode in airtouch_modes] + modes.append(HVAC_MODE_OFF) + return modes + + async def async_set_hvac_mode(self, hvac_mode): + """Set new operation mode.""" + if hvac_mode not in HA_STATE_TO_AT: + raise ValueError(f"Unsupported HVAC mode: {hvac_mode}") + + if hvac_mode == HVAC_MODE_OFF: + return await self.async_turn_off() + await self._airtouch.SetCoolingModeForAc( + self._ac_number, HA_STATE_TO_AT[hvac_mode] + ) + # in case it isn't already, unless the HVAC mode was off, then the ac should be on + await self.async_turn_on() + self._unit = self._airtouch.GetAcs()[self._ac_number] + _LOGGER.debug("Setting operation mode of %s to %s", self._ac_number, hvac_mode) + self.async_write_ha_state() + + async def async_set_fan_mode(self, fan_mode): + """Set new fan mode.""" + if fan_mode not in self.fan_modes: + raise ValueError(f"Unsupported fan mode: {fan_mode}") + + _LOGGER.debug("Setting fan mode of %s to %s", self._ac_number, fan_mode) + await self._airtouch.SetFanSpeedForAc( + self._ac_number, HA_FAN_SPEED_TO_AT[fan_mode] + ) + self._unit = self._airtouch.GetAcs()[self._ac_number] + self.async_write_ha_state() + + async def async_turn_on(self): + """Turn on.""" + _LOGGER.debug("Turning %s on", self.unique_id) + # in case ac is not on. Airtouch turns itself off if no groups are turned on + # (even if groups turned back on) + await self._airtouch.TurnAcOn(self._ac_number) + + async def async_turn_off(self): + """Turn off.""" + _LOGGER.debug("Turning %s off", self.unique_id) + await self._airtouch.TurnAcOff(self._ac_number) + self.async_write_ha_state() + + +class AirtouchGroup(CoordinatorEntity, ClimateEntity): + """Representation of an AirTouch 4 group.""" + + _attr_supported_features = SUPPORT_TARGET_TEMPERATURE + _attr_temperature_unit = TEMP_CELSIUS + _attr_hvac_modes = AT_GROUP_MODES + + def __init__(self, coordinator, group_number, info): + """Initialize the climate device.""" + super().__init__(coordinator) + self._group_number = group_number + self._airtouch = coordinator.airtouch + self._info = info + self._unit = self._airtouch.GetGroupByGroupNumber(self._group_number) + + @callback + def _handle_coordinator_update(self): + self._unit = self._airtouch.GetGroupByGroupNumber(self._group_number) + return super()._handle_coordinator_update() + + @property + def device_info(self): + """Return device info for this device.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Airtouch", + "model": "Airtouch 4", + } + + @property + def unique_id(self): + """Return unique ID for this device.""" + return self._group_number + + @property + def min_temp(self): + """Return Minimum Temperature for AC of this group.""" + return self._airtouch.acs[self._unit.BelongsToAc].MinSetpoint + + @property + def max_temp(self): + """Return Max Temperature for AC of this group.""" + return self._airtouch.acs[self._unit.BelongsToAc].MaxSetpoint + + @property + def name(self): + """Return the name of the climate device.""" + return self._unit.GroupName + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._unit.Temperature + + @property + def target_temperature(self): + """Return the temperature we are trying to reach.""" + return self._unit.TargetSetpoint + + @property + def hvac_mode(self): + """Return hvac target hvac state.""" + # there are other power states that aren't 'on' but still count as on (eg. 'Turbo') + is_off = self._unit.PowerState == "Off" + if is_off: + return HVAC_MODE_OFF + + return HVAC_MODE_FAN_ONLY + + async def async_set_hvac_mode(self, hvac_mode): + """Set new operation mode.""" + if hvac_mode not in HA_STATE_TO_AT: + raise ValueError(f"Unsupported HVAC mode: {hvac_mode}") + + if hvac_mode == HVAC_MODE_OFF: + return await self.async_turn_off() + if self.hvac_mode == HVAC_MODE_OFF: + await self.async_turn_on() + self._unit = self._airtouch.GetGroups()[self._group_number] + _LOGGER.debug( + "Setting operation mode of %s to %s", self._group_number, hvac_mode + ) + self.async_write_ha_state() + + @property + def fan_mode(self): + """Return fan mode of the AC this group belongs to.""" + return AT_TO_HA_FAN_SPEED[self._airtouch.acs[self._unit.BelongsToAc].AcFanSpeed] + + @property + def fan_modes(self): + """Return the list of available fan modes.""" + airtouch_fan_speeds = self._airtouch.GetSupportedFanSpeedsByGroup( + self._group_number + ) + return [AT_TO_HA_FAN_SPEED[speed] for speed in airtouch_fan_speeds] + + async def async_set_temperature(self, **kwargs): + """Set new target temperatures.""" + temp = kwargs.get(ATTR_TEMPERATURE) + + _LOGGER.debug("Setting temp of %s to %s", self._group_number, str(temp)) + self._unit = await self._airtouch.SetGroupToTemperature( + self._group_number, int(temp) + ) + self.async_write_ha_state() + + async def async_set_fan_mode(self, fan_mode): + """Set new fan mode.""" + if fan_mode not in self.fan_modes: + raise ValueError(f"Unsupported fan mode: {fan_mode}") + + _LOGGER.debug("Setting fan mode of %s to %s", self._group_number, fan_mode) + self._unit = await self._airtouch.SetFanSpeedByGroup( + self._group_number, HA_FAN_SPEED_TO_AT[fan_mode] + ) + self.async_write_ha_state() + + async def async_turn_on(self): + """Turn on.""" + _LOGGER.debug("Turning %s on", self.unique_id) + await self._airtouch.TurnGroupOn(self._group_number) + + # in case ac is not on. Airtouch turns itself off if no groups are turned on + # (even if groups turned back on) + await self._airtouch.TurnAcOn( + self._airtouch.GetGroupByGroupNumber(self._group_number).BelongsToAc + ) + # this might cause the ac object to be wrong, so force the shared data + # store to update + await self.coordinator.async_request_refresh() + self.async_write_ha_state() + + async def async_turn_off(self): + """Turn off.""" + _LOGGER.debug("Turning %s off", self.unique_id) + await self._airtouch.TurnGroupOff(self._group_number) + # this will cause the ac object to be wrong + # (ac turns off automatically if no groups are running) + # so force the shared data store to update + await self.coordinator.async_request_refresh() + self.async_write_ha_state() diff --git a/homeassistant/components/airtouch4/config_flow.py b/homeassistant/components/airtouch4/config_flow.py new file mode 100644 index 00000000000..e395c71349b --- /dev/null +++ b/homeassistant/components/airtouch4/config_flow.py @@ -0,0 +1,50 @@ +"""Config flow for AirTouch4.""" +from airtouch4pyapi import AirTouch, AirTouchStatus +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST + +from .const import DOMAIN + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) + + +class AirtouchConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle an Airtouch config flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if user_input is None: + return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) + + errors = {} + + host = user_input[CONF_HOST] + self._async_abort_entries_match({CONF_HOST: host}) + + airtouch = AirTouch(host) + await airtouch.UpdateInfo() + airtouch_status = airtouch.Status + airtouch_has_groups = bool( + airtouch.Status == AirTouchStatus.OK and airtouch.GetGroups() + ) + + if airtouch_status != AirTouchStatus.OK: + errors["base"] = "cannot_connect" + elif not airtouch_has_groups: + errors["base"] = "no_units" + + if errors: + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + return self.async_create_entry( + title=user_input[CONF_HOST], + data={ + CONF_HOST: user_input[CONF_HOST], + }, + ) diff --git a/homeassistant/components/airtouch4/const.py b/homeassistant/components/airtouch4/const.py new file mode 100644 index 00000000000..e110a6cee81 --- /dev/null +++ b/homeassistant/components/airtouch4/const.py @@ -0,0 +1,3 @@ +"""Constants for the AirTouch4 integration.""" + +DOMAIN = "airtouch4" diff --git a/homeassistant/components/airtouch4/manifest.json b/homeassistant/components/airtouch4/manifest.json new file mode 100644 index 00000000000..8297081ae9d --- /dev/null +++ b/homeassistant/components/airtouch4/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "airtouch4", + "name": "AirTouch 4", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/airtouch4", + "requirements": [ + "airtouch4pyapi==1.0.5" + ], + "codeowners": [ + "@LonePurpleWolf" + ], + "iot_class": "local_polling" +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/strings.json b/homeassistant/components/airtouch4/strings.json new file mode 100644 index 00000000000..5259b20fb73 --- /dev/null +++ b/homeassistant/components/airtouch4/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "no_units": "Could not find any AirTouch 4 Groups." + }, + "step": { + "user": { + "title": "Setup your AirTouch 4 connection details.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + } + } + } +} diff --git a/homeassistant/components/airtouch4/translations/en.json b/homeassistant/components/airtouch4/translations/en.json new file mode 100644 index 00000000000..2bde2ea760a --- /dev/null +++ b/homeassistant/components/airtouch4/translations/en.json @@ -0,0 +1,17 @@ +{ + "config": { + + "error": { + "cannot_connect": "Failed to connect", + "no_units": "Could not find any AirTouch 4 Groups." + }, + "step": { + "user": { + "title": "Setup your AirTouch 4.", + "data": { + "host": "Host" + } + } + } + } + } diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b4a6fcc3775..6be4f70b38e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -16,6 +16,7 @@ FLOWS = [ "agent_dvr", "airly", "airnow", + "airtouch4", "airvisual", "alarmdecoder", "almond", diff --git a/requirements_all.txt b/requirements_all.txt index 1f0e4e1b665..5a535b86c13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -257,6 +257,9 @@ aioymaps==1.1.0 # homeassistant.components.airly airly==1.1.0 +# homeassistant.components.airtouch4 +airtouch4pyapi==1.0.5 + # homeassistant.components.aladdin_connect aladdin_connect==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ab3042137d..16591436f25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,6 +178,9 @@ aioymaps==1.1.0 # homeassistant.components.airly airly==1.1.0 +# homeassistant.components.airtouch4 +airtouch4pyapi==1.0.5 + # homeassistant.components.ambee ambee==0.3.0 diff --git a/tests/components/airtouch4/__init__.py b/tests/components/airtouch4/__init__.py new file mode 100644 index 00000000000..cc267ee41d1 --- /dev/null +++ b/tests/components/airtouch4/__init__.py @@ -0,0 +1 @@ +"""Tests for the AirTouch4 integration.""" diff --git a/tests/components/airtouch4/test_config_flow.py b/tests/components/airtouch4/test_config_flow.py new file mode 100644 index 00000000000..a98b24ef88d --- /dev/null +++ b/tests/components/airtouch4/test_config_flow.py @@ -0,0 +1,123 @@ +"""Test the AirTouch 4 config flow.""" +from unittest.mock import AsyncMock, Mock, patch + +from airtouch4pyapi.airtouch import AirTouch, AirTouchAc, AirTouchGroup, AirTouchStatus + +from homeassistant import config_entries +from homeassistant.components.airtouch4.const import DOMAIN + + +async def test_form(hass): + """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"] is None + mock_ac = AirTouchAc() + mock_groups = AirTouchGroup() + mock_airtouch = AirTouch("") + mock_airtouch.UpdateInfo = AsyncMock() + mock_airtouch.Status = AirTouchStatus.OK + mock_airtouch.GetAcs = Mock(return_value=[mock_ac]) + mock_airtouch.GetGroups = Mock(return_value=[mock_groups]) + + with patch( + "homeassistant.components.airtouch4.config_flow.AirTouch", + return_value=mock_airtouch, + ), patch( + "homeassistant.components.airtouch4.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "0.0.0.1"} + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "0.0.0.1" + assert result2["data"] == { + "host": "0.0.0.1", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_timeout(hass): + """Test we handle a connection timeout.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + mock_airtouch = AirTouch("") + mock_airtouch.UpdateInfo = AsyncMock() + mock_airtouch.status = AirTouchStatus.CONNECTION_INTERRUPTED + with patch( + "homeassistant.components.airtouch4.config_flow.AirTouch", + return_value=mock_airtouch, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "0.0.0.1"} + ) + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_library_error_message(hass): + """Test we handle an unknown error message from the library.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + mock_airtouch = AirTouch("") + mock_airtouch.UpdateInfo = AsyncMock() + mock_airtouch.status = AirTouchStatus.ERROR + with patch( + "homeassistant.components.airtouch4.config_flow.AirTouch", + return_value=mock_airtouch, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "0.0.0.1"} + ) + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_connection_refused(hass): + """Test we handle a connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + mock_airtouch = AirTouch("") + mock_airtouch.UpdateInfo = AsyncMock() + mock_airtouch.status = AirTouchStatus.NOT_CONNECTED + with patch( + "homeassistant.components.airtouch4.config_flow.AirTouch", + return_value=mock_airtouch, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "0.0.0.1"} + ) + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_no_units(hass): + """Test we handle no units found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + mock_ac = AirTouchAc() + mock_airtouch = AirTouch("") + mock_airtouch.UpdateInfo = AsyncMock() + mock_airtouch.Status = AirTouchStatus.OK + mock_airtouch.GetAcs = Mock(return_value=[mock_ac]) + mock_airtouch.GetGroups = Mock(return_value=[]) + + with patch( + "homeassistant.components.airtouch4.config_flow.AirTouch", + return_value=mock_airtouch, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "0.0.0.1"} + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "no_units"} From cff6883b5cb58353971453f4f37dbb718c1576b2 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 17 Aug 2021 12:22:27 -0400 Subject: [PATCH 277/355] Add zwave_js Protection CC select entities (#54717) * Add Protection CC select entities comment * Disable entity by default * use class attribute * Enable protection entity by default * add guard for none --- .../components/zwave_js/discovery.py | 10 ++ homeassistant/components/zwave_js/select.py | 36 +++++++ tests/components/zwave_js/test_select.py | 100 ++++++++++++++++++ 3 files changed, 146 insertions(+) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index dcae65b0395..d59a3d935a0 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -646,6 +646,16 @@ DISCOVERY_SCHEMAS = [ ), required_values=[SIREN_TONE_SCHEMA], ), + # select + # protection CC + ZWaveDiscoverySchema( + platform="select", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.PROTECTION}, + property={"local", "rf"}, + type={"number"}, + ), + ), ] diff --git a/homeassistant/components/zwave_js/select.py b/homeassistant/components/zwave_js/select.py index 2bd711bfde3..7aedc6521d9 100644 --- a/homeassistant/components/zwave_js/select.py +++ b/homeassistant/components/zwave_js/select.py @@ -29,6 +29,8 @@ async def async_setup_entry( entities: list[ZWaveBaseEntity] = [] if info.platform_hint == "Default tone": entities.append(ZwaveDefaultToneSelectEntity(config_entry, client, info)) + else: + entities.append(ZwaveSelectEntity(config_entry, client, info)) async_add_entities(entities) config_entry.async_on_unload( @@ -40,6 +42,40 @@ async def async_setup_entry( ) +class ZwaveSelectEntity(ZWaveBaseEntity, SelectEntity): + """Representation of a Z-Wave select entity.""" + + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize a ZwaveSelectEntity entity.""" + super().__init__(config_entry, client, info) + + # Entity class attributes + self._attr_name = self.generate_name(include_value_name=True) + self._attr_options = list(self.info.primary_value.metadata.states.values()) + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + if self.info.primary_value.value is None: + return None + return str( + self.info.primary_value.metadata.states.get( + str(self.info.primary_value.value), self.info.primary_value.value + ) + ) + + async def async_select_option(self, option: str | int) -> None: + """Change the selected option.""" + key = next( + key + for key, val in self.info.primary_value.metadata.states.items() + if val == option + ) + await self.info.node.async_set_value(self.info.primary_value, int(key)) + + class ZwaveDefaultToneSelectEntity(ZWaveBaseEntity, SelectEntity): """Representation of a Z-Wave default tone select entity.""" diff --git a/tests/components/zwave_js/test_select.py b/tests/components/zwave_js/test_select.py index b94bac812b6..43f44f0bba0 100644 --- a/tests/components/zwave_js/test_select.py +++ b/tests/components/zwave_js/test_select.py @@ -1,7 +1,10 @@ """Test the Z-Wave JS number platform.""" from zwave_js_server.event import Event +from homeassistant.const import STATE_UNKNOWN + DEFAULT_TONE_SELECT_ENTITY = "select.indoor_siren_6_default_tone_2" +PROTECTION_SELECT_ENTITY = "select.family_room_combo_local_protection_state" async def test_default_tone_select(hass, client, aeotec_zw164_siren, integration): @@ -99,3 +102,100 @@ async def test_default_tone_select(hass, client, aeotec_zw164_siren, integration state = hass.states.get(DEFAULT_TONE_SELECT_ENTITY) assert state.state == "30DOOR~1 (27 sec)" + + +async def test_protection_select(hass, client, inovelli_lzw36, integration): + """Test the default tone select entity.""" + node = inovelli_lzw36 + state = hass.states.get(PROTECTION_SELECT_ENTITY) + + assert state + assert state.state == "Unprotected" + attr = state.attributes + assert attr["options"] == [ + "Unprotected", + "ProtectedBySequence", + "NoOperationPossible", + ] + + # Test select option with string value + await hass.services.async_call( + "select", + "select_option", + {"entity_id": PROTECTION_SELECT_ENTITY, "option": "ProtectedBySequence"}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == node.node_id + assert args["valueId"] == { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "local", + "propertyName": "local", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "label": "Local protection state", + "states": { + "0": "Unprotected", + "1": "ProtectedBySequence", + "2": "NoOperationPossible", + }, + }, + "value": 0, + } + assert args["value"] == 1 + + client.async_send_command.reset_mock() + + # Test value update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Protection", + "commandClass": 117, + "endpoint": 0, + "property": "local", + "newValue": 1, + "prevValue": 0, + "propertyName": "local", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(PROTECTION_SELECT_ENTITY) + assert state.state == "ProtectedBySequence" + + # Test null value + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Protection", + "commandClass": 117, + "endpoint": 0, + "property": "local", + "newValue": None, + "prevValue": 1, + "propertyName": "local", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(PROTECTION_SELECT_ENTITY) + assert state.state == STATE_UNKNOWN From 15feb430fca10c71d45e1a0bae3c8e305466f924 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 17 Aug 2021 19:15:02 +0200 Subject: [PATCH 278/355] Use DEVICE_CLASS_UPDATE in Synology DSM (#54769) --- homeassistant/components/synology_dsm/const.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index fdbbb5678c2..633c264f3c8 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -10,7 +10,10 @@ from synology_dsm.api.dsm.information import SynoDSMInformation 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 +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_UPDATE, +) from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -81,8 +84,8 @@ UPGRADE_BINARY_SENSORS: dict[str, EntityInfo] = { f"{SynoCoreUpgrade.API_KEY}:update_available": { ATTR_NAME: "Update available", ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_ICON: "mdi:update", - ATTR_DEVICE_CLASS: None, + ATTR_ICON: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_UPDATE, ENTITY_ENABLE: True, ATTR_STATE_CLASS: None, }, From 5b75c8254b747e6583cef23da4b1f5343843c21f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 Aug 2021 19:49:38 +0200 Subject: [PATCH 279/355] Use path helper method for principal file in google_pubsub (#54744) Co-authored-by: Martin Hjelmare --- .../components/google_pubsub/__init__.py | 8 +----- tests/components/google_pubsub/test_init.py | 26 +++++++++---------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index 19530d9d663..d583bc5aac0 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -41,16 +41,10 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass: HomeAssistant, yaml_config: dict[str, Any]): """Activate Google Pub/Sub component.""" - config = yaml_config[DOMAIN] project_id = config[CONF_PROJECT_ID] topic_name = config[CONF_TOPIC_NAME] - if hass.config.config_dir: - service_principal_path = os.path.join( - hass.config.config_dir, config[CONF_SERVICE_PRINCIPAL] - ) - else: - service_principal_path = config[CONF_SERVICE_PRINCIPAL] + service_principal_path = hass.config.path(config[CONF_SERVICE_PRINCIPAL]) if not os.path.isfile(service_principal_path): _LOGGER.error("Path to credentials file cannot be found") diff --git a/tests/components/google_pubsub/test_init.py b/tests/components/google_pubsub/test_init.py index fc1fecb04ed..d31d28e7302 100644 --- a/tests/components/google_pubsub/test_init.py +++ b/tests/components/google_pubsub/test_init.py @@ -1,6 +1,7 @@ """The tests for the Google Pub/Sub component.""" from dataclasses import dataclass from datetime import datetime +import os import unittest.mock as mock import pytest @@ -51,13 +52,12 @@ def mock_client_fixture(): yield client -@pytest.fixture(autouse=True, name="mock_os") -def mock_os_fixture(): - """Mock the OS cli.""" - with mock.patch(f"{GOOGLE_PUBSUB_PATH}.os") as os_cli: - os_cli.path = mock.MagicMock() - setattr(os_cli.path, "join", mock.MagicMock(return_value="path")) - yield os_cli +@pytest.fixture(autouse=True, name="mock_is_file") +def mock_is_file_fixture(): + """Mock os.path.isfile.""" + with mock.patch(f"{GOOGLE_PUBSUB_PATH}.os.path.isfile") as is_file: + is_file.return_value = True + yield is_file @pytest.fixture(autouse=True) @@ -84,9 +84,9 @@ async def test_minimal_config(hass, mock_client): assert hass.bus.listen.called assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED assert mock_client.PublisherClient.from_service_account_json.call_count == 1 - assert ( - mock_client.PublisherClient.from_service_account_json.call_args[0][0] == "path" - ) + assert mock_client.PublisherClient.from_service_account_json.call_args[0][ + 0 + ] == os.path.join(hass.config.config_dir, "creds") async def test_full_config(hass, mock_client): @@ -111,9 +111,9 @@ async def test_full_config(hass, mock_client): assert hass.bus.listen.called assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED assert mock_client.PublisherClient.from_service_account_json.call_count == 1 - assert ( - mock_client.PublisherClient.from_service_account_json.call_args[0][0] == "path" - ) + assert mock_client.PublisherClient.from_service_account_json.call_args[0][ + 0 + ] == os.path.join(hass.config.config_dir, "creds") def make_event(entity_id): From 8bf79d61ee686ff2d977b2ff80dd7a581c960790 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Tue, 17 Aug 2021 12:23:41 -0600 Subject: [PATCH 280/355] Add upnp binary sensor for connectivity status (#54489) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * New binary sensor for connectivity * Add binary_sensor * New binary sensor for connectivity * Add binary_sensor * Handle values returned as None * Small text update for Uptime * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen * Updates based on review * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen * Further updates based on review * Set device_class as a class atribute * Create 1 combined data coordinator and UpnpEntity class * Updates on coordinator * Update comment * Fix in async_step_init for coordinator * Add async_get_status to mocked device and set times polled for each call seperately * Updated to get device through coordinator Check polling for each status call seperately * Use collections.abc instead of Typing for Mapping * Remove adding device to hass.data as coordinator is now saved * Removed setting _coordinator * Added myself as codeowner * Update type in __init__ * Removed attributes from binary sensor * Fix async_unload_entry * Add expected return value to is_on Co-authored-by: Joakim Sørensen --- CODEOWNERS | 2 +- homeassistant/components/upnp/__init__.py | 97 ++++++++++++--- .../components/upnp/binary_sensor.py | 54 +++++++++ homeassistant/components/upnp/config_flow.py | 30 +++-- homeassistant/components/upnp/const.py | 3 + homeassistant/components/upnp/device.py | 18 +++ homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/upnp/sensor.py | 111 +++++------------- tests/components/upnp/mock_upnp_device.py | 17 ++- tests/components/upnp/test_config_flow.py | 18 +-- 10 files changed, 222 insertions(+), 130 deletions(-) create mode 100644 homeassistant/components/upnp/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 1dedb1d421b..85b89649a99 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -545,7 +545,7 @@ homeassistant/components/upb/* @gwww homeassistant/components/upc_connect/* @pvizeli @fabaff homeassistant/components/upcloud/* @scop homeassistant/components/updater/* @home-assistant/core -homeassistant/components/upnp/* @StevenLooman +homeassistant/components/upnp/* @StevenLooman @ehendrix23 homeassistant/components/uptimerobot/* @ludeeus homeassistant/components/usgs_earthquakes_feed/* @exxamalte homeassistant/components/utility_meter/* @dgomes diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 08e6a35f5b3..c21c1d24f0c 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Mapping +from datetime import timedelta from ipaddress import ip_address from typing import Any @@ -17,24 +18,30 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import ( CONF_LOCAL_IP, CONFIG_ENTRY_HOSTNAME, + CONFIG_ENTRY_SCAN_INTERVAL, CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, + DEFAULT_SCAN_INTERVAL, DOMAIN, DOMAIN_CONFIG, DOMAIN_DEVICES, DOMAIN_LOCAL_IP, - LOGGER as _LOGGER, + LOGGER, ) from .device import Device NOTIFICATION_ID = "upnp_notification" NOTIFICATION_TITLE = "UPnP/IGD Setup" -PLATFORMS = ["sensor"] +PLATFORMS = ["binary_sensor", "sensor"] CONFIG_SCHEMA = vol.Schema( { @@ -50,7 +57,7 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType): """Set up UPnP component.""" - _LOGGER.debug("async_setup, config: %s", config) + LOGGER.debug("async_setup, config: %s", config) conf_default = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] conf = config.get(DOMAIN, conf_default) local_ip = await async_get_source_ip(hass, PUBLIC_TARGET_IP) @@ -73,7 +80,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up UPnP/IGD device from a config entry.""" - _LOGGER.debug("Setting up config entry: %s", entry.unique_id) + LOGGER.debug("Setting up config entry: %s", entry.unique_id) udn = entry.data[CONFIG_ENTRY_UDN] st = entry.data[CONFIG_ENTRY_ST] # pylint: disable=invalid-name @@ -86,7 +93,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @callback def device_discovered(info: Mapping[str, Any]) -> None: nonlocal discovery_info - _LOGGER.debug( + LOGGER.debug( "Device discovered: %s, at: %s", usn, info[ssdp.ATTR_SSDP_LOCATION] ) discovery_info = info @@ -103,7 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await asyncio.wait_for(device_discovered_event.wait(), timeout=10) except asyncio.TimeoutError as err: - _LOGGER.debug("Device not discovered: %s", usn) + LOGGER.debug("Device not discovered: %s", usn) raise ConfigEntryNotReady from err finally: cancel_discovered_callback() @@ -114,12 +121,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ] device = await Device.async_create_device(hass, location) - # Save device. - hass.data[DOMAIN][DOMAIN_DEVICES][udn] = device - # Ensure entry has a unique_id. if not entry.unique_id: - _LOGGER.debug( + LOGGER.debug( "Setting unique_id: %s, for config_entry: %s", device.unique_id, entry, @@ -150,8 +154,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: model=device.model_name, ) + update_interval_sec = entry.options.get( + CONFIG_ENTRY_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ) + update_interval = timedelta(seconds=update_interval_sec) + LOGGER.debug("update_interval: %s", update_interval) + coordinator = UpnpDataUpdateCoordinator( + hass, + device=device, + update_interval=update_interval, + ) + + # Save coordinator. + hass.data[DOMAIN][entry.entry_id] = coordinator + + await coordinator.async_config_entry_first_refresh() + # Create sensors. - _LOGGER.debug("Enabling sensors") + LOGGER.debug("Enabling sensors") hass.config_entries.async_setup_platforms(entry, PLATFORMS) # Start device updater. @@ -162,14 +182,53 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a UPnP/IGD device from a config entry.""" - _LOGGER.debug("Unloading config entry: %s", config_entry.unique_id) + LOGGER.debug("Unloading config entry: %s", config_entry.unique_id) - udn = config_entry.data.get(CONFIG_ENTRY_UDN) - if udn in hass.data[DOMAIN][DOMAIN_DEVICES]: - device = hass.data[DOMAIN][DOMAIN_DEVICES][udn] - await device.async_stop() + if coordinator := hass.data[DOMAIN].pop(config_entry.entry_id, None): + await coordinator.device.async_stop() - del hass.data[DOMAIN][DOMAIN_DEVICES][udn] - - _LOGGER.debug("Deleting sensors") + LOGGER.debug("Deleting sensors") return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) + + +class UpnpDataUpdateCoordinator(DataUpdateCoordinator): + """Define an object to update data from UPNP device.""" + + def __init__( + self, hass: HomeAssistant, device: Device, update_interval: timedelta + ) -> None: + """Initialize.""" + self.device = device + + super().__init__( + hass, LOGGER, name=device.name, update_interval=update_interval + ) + + async def _async_update_data(self) -> Mapping[str, Any]: + """Update data.""" + update_values = await asyncio.gather( + self.device.async_get_traffic_data(), + self.device.async_get_status(), + ) + + data = dict(update_values[0]) + data.update(update_values[1]) + + return data + + +class UpnpEntity(CoordinatorEntity): + """Base class for UPnP/IGD entities.""" + + coordinator: UpnpDataUpdateCoordinator + + def __init__(self, coordinator: UpnpDataUpdateCoordinator) -> None: + """Initialize the base entities.""" + super().__init__(coordinator) + self._device = coordinator.device + self._attr_device_info = { + "connections": {(dr.CONNECTION_UPNP, coordinator.device.udn)}, + "name": coordinator.device.name, + "manufacturer": coordinator.device.manufacturer, + "model": coordinator.device.model_name, + } diff --git a/homeassistant/components/upnp/binary_sensor.py b/homeassistant/components/upnp/binary_sensor.py new file mode 100644 index 00000000000..2f2f0af0e96 --- /dev/null +++ b/homeassistant/components/upnp/binary_sensor.py @@ -0,0 +1,54 @@ +"""Support for UPnP/IGD Binary Sensors.""" +from __future__ import annotations + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import UpnpDataUpdateCoordinator, UpnpEntity +from .const import DOMAIN, LOGGER, WANSTATUS + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the UPnP/IGD sensors.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + + LOGGER.debug("Adding binary sensor") + + sensors = [ + UpnpStatusBinarySensor(coordinator), + ] + async_add_entities(sensors) + + +class UpnpStatusBinarySensor(UpnpEntity, BinarySensorEntity): + """Class for UPnP/IGD binary sensors.""" + + _attr_device_class = DEVICE_CLASS_CONNECTIVITY + + def __init__( + self, + coordinator: UpnpDataUpdateCoordinator, + ) -> None: + """Initialize the base sensor.""" + super().__init__(coordinator) + self._attr_name = f"{coordinator.device.name} wan status" + self._attr_unique_id = f"{coordinator.device.udn}_wanstatus" + + @property + def available(self) -> bool: + """Return if entity is available.""" + return super().available and self.coordinator.data.get(WANSTATUS) + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + return self.coordinator.data[WANSTATUS] == "Connected" diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 89e1e5c71d0..5df4e267427 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -20,8 +20,7 @@ from .const import ( CONFIG_ENTRY_UDN, DEFAULT_SCAN_INTERVAL, DOMAIN, - DOMAIN_DEVICES, - LOGGER as _LOGGER, + LOGGER, SSDP_SEARCH_TIMEOUT, ST_IGD_V1, ST_IGD_V2, @@ -43,7 +42,7 @@ async def _async_wait_for_discoveries(hass: HomeAssistant) -> bool: @callback def device_discovered(info: Mapping[str, Any]) -> None: - _LOGGER.info( + LOGGER.info( "Device discovered: %s, at: %s", info[ssdp.ATTR_SSDP_USN], info[ssdp.ATTR_SSDP_LOCATION], @@ -103,7 +102,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: Mapping | None = None ) -> Mapping[str, Any]: """Handle a flow start.""" - _LOGGER.debug("async_step_user: user_input: %s", user_input) + LOGGER.debug("async_step_user: user_input: %s", user_input) if user_input is not None: # Ensure wanted device was discovered. @@ -162,12 +161,12 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): configured before, find any device and create a config_entry for it. Otherwise, do nothing. """ - _LOGGER.debug("async_step_import: import_info: %s", import_info) + LOGGER.debug("async_step_import: import_info: %s", import_info) # Landed here via configuration.yaml entry. # Any device already added, then abort. if self._async_current_entries(): - _LOGGER.debug("Already configured, aborting") + LOGGER.debug("Already configured, aborting") return self.async_abort(reason="already_configured") # Discover devices. @@ -176,7 +175,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Ensure anything to add. If not, silently abort. if not discoveries: - _LOGGER.info("No UPnP devices discovered, aborting") + LOGGER.info("No UPnP devices discovered, aborting") return self.async_abort(reason="no_devices_found") # Ensure complete discovery. @@ -187,7 +186,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): or ssdp.ATTR_SSDP_LOCATION not in discovery or ssdp.ATTR_SSDP_USN not in discovery ): - _LOGGER.debug("Incomplete discovery, ignoring") + LOGGER.debug("Incomplete discovery, ignoring") return self.async_abort(reason="incomplete_discovery") # Ensure not already configuring/configured. @@ -202,7 +201,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): This flow is triggered by the SSDP component. It will check if the host is already configured and delegate to the import step if not. """ - _LOGGER.debug("async_step_ssdp: discovery_info: %s", discovery_info) + LOGGER.debug("async_step_ssdp: discovery_info: %s", discovery_info) # Ensure complete discovery. if ( @@ -211,7 +210,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): or ssdp.ATTR_SSDP_LOCATION not in discovery_info or ssdp.ATTR_SSDP_USN not in discovery_info ): - _LOGGER.debug("Incomplete discovery, ignoring") + LOGGER.debug("Incomplete discovery, ignoring") return self.async_abort(reason="incomplete_discovery") # Ensure not already configuring/configured. @@ -225,7 +224,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): for config_entry in existing_entries: entry_hostname = config_entry.data.get(CONFIG_ENTRY_HOSTNAME) if entry_hostname == hostname: - _LOGGER.debug( + LOGGER.debug( "Found existing config_entry with same hostname, discovery ignored" ) return self.async_abort(reason="discovery_ignored") @@ -244,7 +243,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: Mapping | None = None ) -> Mapping[str, Any]: """Confirm integration via SSDP.""" - _LOGGER.debug("async_step_ssdp_confirm: user_input: %s", user_input) + LOGGER.debug("async_step_ssdp_confirm: user_input: %s", user_input) if user_input is None: return self.async_show_form(step_id="ssdp_confirm") @@ -264,7 +263,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): discovery: Mapping, ) -> Mapping[str, Any]: """Create an entry from discovery.""" - _LOGGER.debug( + LOGGER.debug( "_async_create_entry_from_discovery: discovery: %s", discovery, ) @@ -288,13 +287,12 @@ class UpnpOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input: Mapping = None) -> None: """Manage the options.""" if user_input is not None: - udn = self.config_entry.data[CONFIG_ENTRY_UDN] - coordinator = self.hass.data[DOMAIN][DOMAIN_DEVICES][udn].coordinator + coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id] update_interval_sec = user_input.get( CONFIG_ENTRY_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL ) update_interval = timedelta(seconds=update_interval_sec) - _LOGGER.debug("Updating coordinator, update_interval: %s", update_interval) + LOGGER.debug("Updating coordinator, update_interval: %s", update_interval) coordinator.update_interval = update_interval return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index cbb071bc15e..769e398c5a4 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -18,6 +18,9 @@ PACKETS_SENT = "packets_sent" TIMESTAMP = "timestamp" DATA_PACKETS = "packets" DATA_RATE_PACKETS_PER_SECOND = f"{DATA_PACKETS}/{TIME_SECONDS}" +WANSTATUS = "wan_status" +WANIP = "wan_ip" +UPTIME = "uptime" KIBIBYTE = 1024 UPDATE_INTERVAL = timedelta(seconds=30) CONFIG_ENTRY_SCAN_INTERVAL = "scan_interval" diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 5e6f8ef5023..ca06f501405 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -27,6 +27,9 @@ from .const import ( PACKETS_RECEIVED, PACKETS_SENT, TIMESTAMP, + UPTIME, + WANIP, + WANSTATUS, ) @@ -154,3 +157,18 @@ class Device: PACKETS_RECEIVED: values[2], PACKETS_SENT: values[3], } + + async def async_get_status(self) -> Mapping[str, Any]: + """Get connection status, uptime, and external IP.""" + _LOGGER.debug("Getting status for device: %s", self) + + values = await asyncio.gather( + self._igd_device.async_get_status_info(), + self._igd_device.async_get_external_ip_address(), + ) + + return { + WANSTATUS: values[0][0] if values[0] is not None else None, + UPTIME: values[0][2] if values[0] is not None else None, + WANIP: values[1], + } diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 937518c34ac..fc8ba185d3c 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/upnp", "requirements": ["async-upnp-client==0.19.2"], "dependencies": ["network", "ssdp"], - "codeowners": ["@StevenLooman"], + "codeowners": ["@StevenLooman","@ehendrix23"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 82df1f59469..185d3ecac6d 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -1,38 +1,25 @@ """Support for UPnP/IGD Sensors.""" from __future__ import annotations -from datetime import timedelta -from typing import Any, Mapping - from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_BYTES, DATA_RATE_KIBIBYTES_PER_SECOND from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from . import UpnpDataUpdateCoordinator, UpnpEntity from .const import ( BYTES_RECEIVED, BYTES_SENT, - CONFIG_ENTRY_SCAN_INTERVAL, - CONFIG_ENTRY_UDN, DATA_PACKETS, DATA_RATE_PACKETS_PER_SECOND, - DEFAULT_SCAN_INTERVAL, DOMAIN, - DOMAIN_DEVICES, KIBIBYTE, - LOGGER as _LOGGER, + LOGGER, PACKETS_RECEIVED, PACKETS_SENT, TIMESTAMP, ) -from .device import Device SENSOR_TYPES = { BYTES_RECEIVED: { @@ -78,7 +65,7 @@ async def async_setup_platform( hass: HomeAssistant, config, async_add_entities, discovery_info=None ) -> None: """Old way of setting up UPnP/IGD sensors.""" - _LOGGER.debug( + LOGGER.debug( "async_setup_platform: config: %s, discovery: %s", config, discovery_info ) @@ -89,52 +76,36 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the UPnP/IGD sensors.""" - udn = config_entry.data[CONFIG_ENTRY_UDN] - device: Device = hass.data[DOMAIN][DOMAIN_DEVICES][udn] + coordinator = hass.data[DOMAIN][config_entry.entry_id] - update_interval_sec = config_entry.options.get( - CONFIG_ENTRY_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL - ) - update_interval = timedelta(seconds=update_interval_sec) - _LOGGER.debug("update_interval: %s", update_interval) - _LOGGER.debug("Adding sensors") - coordinator = DataUpdateCoordinator[Mapping[str, Any]]( - hass, - _LOGGER, - name=device.name, - update_method=device.async_get_traffic_data, - update_interval=update_interval, - ) - device.coordinator = coordinator - - await coordinator.async_refresh() + LOGGER.debug("Adding sensors") sensors = [ - RawUpnpSensor(coordinator, device, SENSOR_TYPES[BYTES_RECEIVED]), - RawUpnpSensor(coordinator, device, SENSOR_TYPES[BYTES_SENT]), - RawUpnpSensor(coordinator, device, SENSOR_TYPES[PACKETS_RECEIVED]), - RawUpnpSensor(coordinator, device, SENSOR_TYPES[PACKETS_SENT]), - DerivedUpnpSensor(coordinator, device, SENSOR_TYPES[BYTES_RECEIVED]), - DerivedUpnpSensor(coordinator, device, SENSOR_TYPES[BYTES_SENT]), - DerivedUpnpSensor(coordinator, device, SENSOR_TYPES[PACKETS_RECEIVED]), - DerivedUpnpSensor(coordinator, device, SENSOR_TYPES[PACKETS_SENT]), + RawUpnpSensor(coordinator, SENSOR_TYPES[BYTES_RECEIVED]), + RawUpnpSensor(coordinator, SENSOR_TYPES[BYTES_SENT]), + RawUpnpSensor(coordinator, SENSOR_TYPES[PACKETS_RECEIVED]), + RawUpnpSensor(coordinator, SENSOR_TYPES[PACKETS_SENT]), + DerivedUpnpSensor(coordinator, SENSOR_TYPES[BYTES_RECEIVED]), + DerivedUpnpSensor(coordinator, SENSOR_TYPES[BYTES_SENT]), + DerivedUpnpSensor(coordinator, SENSOR_TYPES[PACKETS_RECEIVED]), + DerivedUpnpSensor(coordinator, SENSOR_TYPES[PACKETS_SENT]), ] - async_add_entities(sensors, True) + async_add_entities(sensors) -class UpnpSensor(CoordinatorEntity, SensorEntity): +class UpnpSensor(UpnpEntity, SensorEntity): """Base class for UPnP/IGD sensors.""" def __init__( self, - coordinator: DataUpdateCoordinator[Mapping[str, Any]], - device: Device, - sensor_type: Mapping[str, str], + coordinator: UpnpDataUpdateCoordinator, + sensor_type: dict[str, str], ) -> None: """Initialize the base sensor.""" super().__init__(coordinator) - self._device = device self._sensor_type = sensor_type + self._attr_name = f"{coordinator.device.name} {sensor_type['name']}" + self._attr_unique_id = f"{coordinator.device.udn}_{sensor_type['unique_id']}" @property def icon(self) -> str: @@ -144,37 +115,15 @@ class UpnpSensor(CoordinatorEntity, SensorEntity): @property def available(self) -> bool: """Return if entity is available.""" - device_value_key = self._sensor_type["device_value_key"] - return ( - self.coordinator.last_update_success - and device_value_key in self.coordinator.data + return super().available and self.coordinator.data.get( + self._sensor_type["device_value_key"] ) - @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"{self._device.name} {self._sensor_type['name']}" - - @property - def unique_id(self) -> str: - """Return an unique ID.""" - return f"{self._device.udn}_{self._sensor_type['unique_id']}" - @property def native_unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" return self._sensor_type["unit"] - @property - def device_info(self) -> DeviceInfo: - """Get device info.""" - return { - "connections": {(dr.CONNECTION_UPNP, self._device.udn)}, - "name": self._device.name, - "manufacturer": self._device.manufacturer, - "model": self._device.model_name, - } - class RawUpnpSensor(UpnpSensor): """Representation of a UPnP/IGD sensor.""" @@ -192,21 +141,15 @@ class RawUpnpSensor(UpnpSensor): class DerivedUpnpSensor(UpnpSensor): """Representation of a UNIT Sent/Received per second sensor.""" - def __init__(self, coordinator, device, sensor_type) -> None: + def __init__(self, coordinator: UpnpDataUpdateCoordinator, sensor_type) -> None: """Initialize sensor.""" - super().__init__(coordinator, device, sensor_type) + super().__init__(coordinator, sensor_type) self._last_value = None self._last_timestamp = None - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"{self._device.name} {self._sensor_type['derived_name']}" - - @property - def unique_id(self) -> str: - """Return an unique ID.""" - return f"{self._device.udn}_{self._sensor_type['derived_unique_id']}" + self._attr_name = f"{coordinator.device.name} {sensor_type['derived_name']}" + self._attr_unique_id = ( + f"{coordinator.device.udn}_{sensor_type['derived_unique_id']}" + ) @property def native_unit_of_measurement(self) -> str: diff --git a/tests/components/upnp/mock_upnp_device.py b/tests/components/upnp/mock_upnp_device.py index 78adbc5e220..42c9291f30f 100644 --- a/tests/components/upnp/mock_upnp_device.py +++ b/tests/components/upnp/mock_upnp_device.py @@ -11,6 +11,9 @@ from homeassistant.components.upnp.const import ( PACKETS_RECEIVED, PACKETS_SENT, TIMESTAMP, + UPTIME, + WANIP, + WANSTATUS, ) from homeassistant.components.upnp.device import Device from homeassistant.util import dt @@ -27,7 +30,8 @@ class MockDevice(Device): mock_device_updater = AsyncMock() super().__init__(igd_device, mock_device_updater) self._udn = udn - self.times_polled = 0 + self.traffic_times_polled = 0 + self.status_times_polled = 0 @classmethod async def async_create_device(cls, hass, ssdp_location) -> "MockDevice": @@ -66,7 +70,7 @@ class MockDevice(Device): async def async_get_traffic_data(self) -> Mapping[str, Any]: """Get traffic data.""" - self.times_polled += 1 + self.traffic_times_polled += 1 return { TIMESTAMP: dt.utcnow(), BYTES_RECEIVED: 0, @@ -75,6 +79,15 @@ class MockDevice(Device): PACKETS_SENT: 0, } + async def async_get_status(self) -> Mapping[str, Any]: + """Get connection status, uptime, and external IP.""" + self.status_times_polled += 1 + return { + WANSTATUS: "Connected", + UPTIME: 0, + WANIP: "192.168.0.1", + } + async def async_start(self) -> None: """Start the device updater.""" diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index 646bdb143e9..907fa709c84 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -14,7 +14,6 @@ from homeassistant.components.upnp.const import ( CONFIG_ENTRY_UDN, DEFAULT_SCAN_INTERVAL, DOMAIN, - DOMAIN_DEVICES, ) from homeassistant.core import CoreState, HomeAssistant from homeassistant.setup import async_setup_component @@ -238,15 +237,17 @@ async def test_options_flow(hass: HomeAssistant): config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) is True await hass.async_block_till_done() - mock_device = hass.data[DOMAIN][DOMAIN_DEVICES][TEST_UDN] + mock_device = hass.data[DOMAIN][config_entry.entry_id].device # Reset. - mock_device.times_polled = 0 + mock_device.traffic_times_polled = 0 + mock_device.status_times_polled = 0 # Forward time, ensure single poll after 30 (default) seconds. async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=31)) await hass.async_block_till_done() - assert mock_device.times_polled == 1 + assert mock_device.traffic_times_polled == 1 + assert mock_device.status_times_polled == 1 # Options flow with no input results in form. result = await hass.config_entries.options.async_init( @@ -267,15 +268,18 @@ async def test_options_flow(hass: HomeAssistant): # Forward time, ensure single poll after 60 seconds, still from original setting. async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=61)) await hass.async_block_till_done() - assert mock_device.times_polled == 2 + assert mock_device.traffic_times_polled == 2 + assert mock_device.status_times_polled == 2 # Now the updated interval takes effect. # Forward time, ensure single poll after 120 seconds. async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=121)) await hass.async_block_till_done() - assert mock_device.times_polled == 3 + assert mock_device.traffic_times_polled == 3 + assert mock_device.status_times_polled == 3 # Forward time, ensure single poll after 180 seconds. async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=181)) await hass.async_block_till_done() - assert mock_device.times_polled == 4 + assert mock_device.traffic_times_polled == 4 + assert mock_device.status_times_polled == 4 From 71b0f6d095603e2c574a46763765f079d367dc2e Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 17 Aug 2021 20:43:27 +0200 Subject: [PATCH 281/355] set common test entity name. (#54697) --- tests/components/modbus/conftest.py | 23 ++- tests/components/modbus/test_binary_sensor.py | 17 +- tests/components/modbus/test_climate.py | 26 ++- tests/components/modbus/test_cover.py | 25 ++- tests/components/modbus/test_fan.py | 44 +++--- tests/components/modbus/test_init.py | 148 +++++++++--------- tests/components/modbus/test_light.py | 42 ++--- tests/components/modbus/test_sensor.py | 47 +++--- tests/components/modbus/test_switch.py | 52 +++--- 9 files changed, 226 insertions(+), 198 deletions(-) diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index a33d0932c1d..4f2c9b2b778 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -7,7 +7,11 @@ from unittest import mock from pymodbus.exceptions import ModbusException import pytest -from homeassistant.components.modbus.const import DEFAULT_HUB, MODBUS_DOMAIN as DOMAIN +from homeassistant.components.modbus.const import ( + CONF_TCP, + DEFAULT_HUB, + MODBUS_DOMAIN as DOMAIN, +) from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -22,6 +26,11 @@ import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed, mock_restore_cache TEST_MODBUS_NAME = "modbusTest" +TEST_ENTITY_NAME = "test_entity" +TEST_MODBUS_HOST = "modbusHost" +TEST_PORT_TCP = 5501 +TEST_PORT_SERIAL = "usb01" + _LOGGER = logging.getLogger(__name__) @@ -62,9 +71,9 @@ async def mock_modbus(hass, caplog, request, do_config): config = { DOMAIN: [ { - CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", - CONF_PORT: 5501, + CONF_TYPE: CONF_TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, **do_config, } @@ -122,9 +131,9 @@ async def base_test( config_modbus = { DOMAIN: { CONF_NAME: DEFAULT_HUB, - CONF_TYPE: "tcp", - CONF_HOST: "modbusTest", - CONF_PORT: 5001, + CONF_TYPE: CONF_TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, }, } diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index dc9a547dc18..fb52ea11090 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -20,10 +20,9 @@ from homeassistant.const import ( ) from homeassistant.core import State -from .conftest import ReadResult, base_test +from .conftest import TEST_ENTITY_NAME, ReadResult, base_test -SENSOR_NAME = "test_binary_sensor" -ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" +ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}" @pytest.mark.parametrize( @@ -32,7 +31,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" { CONF_BINARY_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, } ] @@ -40,7 +39,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" { CONF_BINARY_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_SLAVE: 10, CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, @@ -89,8 +88,8 @@ async def test_all_binary_sensor(hass, do_type, regs, expected): """Run test for given config.""" state = await base_test( hass, - {CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: do_type}, - SENSOR_NAME, + {CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: do_type}, + TEST_ENTITY_NAME, SENSOR_DOMAIN, CONF_BINARY_SENSORS, None, @@ -108,7 +107,7 @@ async def test_all_binary_sensor(hass, do_type, regs, expected): { CONF_BINARY_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: CALL_TYPE_COIL, } @@ -144,7 +143,7 @@ async def test_service_binary_sensor_update(hass, mock_modbus, mock_ha): { CONF_BINARY_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_SCAN_INTERVAL: 0, } diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index 71dbb6aa8a7..16ef18a60ac 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -22,10 +22,9 @@ from homeassistant.const import ( ) from homeassistant.core import State -from .conftest import ReadResult, base_test +from .conftest import TEST_ENTITY_NAME, ReadResult, base_test -CLIMATE_NAME = "test_climate" -ENTITY_ID = f"{CLIMATE_DOMAIN}.{CLIMATE_NAME}" +ENTITY_ID = f"{CLIMATE_DOMAIN}.{TEST_ENTITY_NAME}" @pytest.mark.parametrize( @@ -34,7 +33,7 @@ ENTITY_ID = f"{CLIMATE_DOMAIN}.{CLIMATE_NAME}" { CONF_CLIMATES: [ { - CONF_NAME: CLIMATE_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SLAVE: 10, @@ -44,7 +43,7 @@ ENTITY_ID = f"{CLIMATE_DOMAIN}.{CLIMATE_NAME}" { CONF_CLIMATES: [ { - CONF_NAME: CLIMATE_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SLAVE: 10, @@ -71,18 +70,17 @@ async def test_config_climate(hass, mock_modbus): ) async def test_temperature_climate(hass, regs, expected): """Run test for given config.""" - CLIMATE_NAME = "modbus_test_climate" return state = await base_test( hass, { - CONF_NAME: CLIMATE_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_SLAVE: 1, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_COUNT: 2, }, - CLIMATE_NAME, + TEST_ENTITY_NAME, CLIMATE_DOMAIN, CONF_CLIMATES, None, @@ -100,7 +98,7 @@ async def test_temperature_climate(hass, regs, expected): { CONF_CLIMATES: [ { - CONF_NAME: CLIMATE_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SLAVE: 10, @@ -127,7 +125,7 @@ async def test_service_climate_update(hass, mock_modbus, mock_ha): { CONF_CLIMATES: [ { - CONF_NAME: CLIMATE_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SLAVE: 10, @@ -142,7 +140,7 @@ async def test_service_climate_update(hass, mock_modbus, mock_ha): { CONF_CLIMATES: [ { - CONF_NAME: CLIMATE_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SLAVE: 10, @@ -157,7 +155,7 @@ async def test_service_climate_update(hass, mock_modbus, mock_ha): { CONF_CLIMATES: [ { - CONF_NAME: CLIMATE_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SLAVE: 10, @@ -172,7 +170,7 @@ async def test_service_climate_update(hass, mock_modbus, mock_ha): { CONF_CLIMATES: [ { - CONF_NAME: CLIMATE_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SLAVE: 10, @@ -214,7 +212,7 @@ test_value.attributes = {ATTR_TEMPERATURE: 37} { CONF_CLIMATES: [ { - CONF_NAME: CLIMATE_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SCAN_INTERVAL: 0, diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py index b1add3e3745..266193294c6 100644 --- a/tests/components/modbus/test_cover.py +++ b/tests/components/modbus/test_cover.py @@ -29,10 +29,9 @@ from homeassistant.const import ( ) from homeassistant.core import State -from .conftest import ReadResult, base_test +from .conftest import TEST_ENTITY_NAME, ReadResult, base_test -COVER_NAME = "test_cover" -ENTITY_ID = f"{COVER_DOMAIN}.{COVER_NAME}" +ENTITY_ID = f"{COVER_DOMAIN}.{TEST_ENTITY_NAME}" @pytest.mark.parametrize( @@ -41,7 +40,7 @@ ENTITY_ID = f"{COVER_DOMAIN}.{COVER_NAME}" { CONF_COVERS: [ { - CONF_NAME: COVER_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: CALL_TYPE_COIL, } @@ -50,7 +49,7 @@ ENTITY_ID = f"{COVER_DOMAIN}.{COVER_NAME}" { CONF_COVERS: [ { - CONF_NAME: COVER_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SLAVE: 10, @@ -95,12 +94,12 @@ async def test_coil_cover(hass, regs, expected): state = await base_test( hass, { - CONF_NAME: COVER_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_INPUT_TYPE: CALL_TYPE_COIL, CONF_ADDRESS: 1234, CONF_SLAVE: 1, }, - COVER_NAME, + TEST_ENTITY_NAME, COVER_DOMAIN, CONF_COVERS, None, @@ -142,11 +141,11 @@ async def test_register_cover(hass, regs, expected): state = await base_test( hass, { - CONF_NAME: COVER_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, }, - COVER_NAME, + TEST_ENTITY_NAME, COVER_DOMAIN, CONF_COVERS, None, @@ -164,7 +163,7 @@ async def test_register_cover(hass, regs, expected): { CONF_COVERS: [ { - CONF_NAME: COVER_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, } @@ -201,7 +200,7 @@ async def test_service_cover_update(hass, mock_modbus, mock_ha): { CONF_COVERS: [ { - CONF_NAME: COVER_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_INPUT_TYPE: CALL_TYPE_COIL, CONF_ADDRESS: 1234, CONF_STATE_OPEN: 1, @@ -228,13 +227,13 @@ async def test_restore_state_cover(hass, mock_test_state, mock_modbus): { CONF_COVERS: [ { - CONF_NAME: COVER_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SCAN_INTERVAL: 0, }, { - CONF_NAME: f"{COVER_NAME}2", + CONF_NAME: f"{TEST_ENTITY_NAME}2", CONF_INPUT_TYPE: CALL_TYPE_COIL, CONF_ADDRESS: 1234, CONF_SCAN_INTERVAL: 0, diff --git a/tests/components/modbus/test_fan.py b/tests/components/modbus/test_fan.py index e0d23ad48db..4aa55473737 100644 --- a/tests/components/modbus/test_fan.py +++ b/tests/components/modbus/test_fan.py @@ -12,6 +12,7 @@ from homeassistant.components.modbus.const import ( CONF_INPUT_TYPE, CONF_STATE_OFF, CONF_STATE_ON, + CONF_TCP, CONF_VERIFY, CONF_WRITE_TYPE, MODBUS_DOMAIN, @@ -33,10 +34,15 @@ from homeassistant.const import ( from homeassistant.core import State from homeassistant.setup import async_setup_component -from .conftest import ReadResult, base_test +from .conftest import ( + TEST_ENTITY_NAME, + TEST_MODBUS_HOST, + TEST_PORT_TCP, + ReadResult, + base_test, +) -FAN_NAME = "test_fan" -ENTITY_ID = f"{FAN_DOMAIN}.{FAN_NAME}" +ENTITY_ID = f"{FAN_DOMAIN}.{TEST_ENTITY_NAME}" @pytest.mark.parametrize( @@ -45,7 +51,7 @@ ENTITY_ID = f"{FAN_DOMAIN}.{FAN_NAME}" { CONF_FANS: [ { - CONF_NAME: FAN_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, } ] @@ -53,7 +59,7 @@ ENTITY_ID = f"{FAN_DOMAIN}.{FAN_NAME}" { CONF_FANS: [ { - CONF_NAME: FAN_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, } @@ -62,7 +68,7 @@ ENTITY_ID = f"{FAN_DOMAIN}.{FAN_NAME}" { CONF_FANS: [ { - CONF_NAME: FAN_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -79,7 +85,7 @@ ENTITY_ID = f"{FAN_DOMAIN}.{FAN_NAME}" { CONF_FANS: [ { - CONF_NAME: FAN_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -96,7 +102,7 @@ ENTITY_ID = f"{FAN_DOMAIN}.{FAN_NAME}" { CONF_FANS: [ { - CONF_NAME: FAN_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -113,7 +119,7 @@ ENTITY_ID = f"{FAN_DOMAIN}.{FAN_NAME}" { CONF_FANS: [ { - CONF_NAME: FAN_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -165,13 +171,13 @@ async def test_all_fan(hass, call_type, regs, verify, expected): state = await base_test( hass, { - CONF_NAME: FAN_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_WRITE_TYPE: call_type, **verify, }, - FAN_NAME, + TEST_ENTITY_NAME, FAN_DOMAIN, CONF_FANS, None, @@ -194,7 +200,7 @@ async def test_all_fan(hass, call_type, regs, verify, expected): { CONF_FANS: [ { - CONF_NAME: FAN_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SCAN_INTERVAL: 0, } @@ -210,21 +216,21 @@ async def test_restore_state_fan(hass, mock_test_state, mock_modbus): async def test_fan_service_turn(hass, caplog, mock_pymodbus): """Run test for service turn_on/turn_off.""" - ENTITY_ID2 = f"{FAN_DOMAIN}.{FAN_NAME}2" + ENTITY_ID2 = f"{FAN_DOMAIN}.{TEST_ENTITY_NAME}2" config = { MODBUS_DOMAIN: { - CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", - CONF_PORT: 5501, + CONF_TYPE: CONF_TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_FANS: [ { - CONF_NAME: FAN_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SCAN_INTERVAL: 0, }, { - CONF_NAME: f"{FAN_NAME}2", + CONF_NAME: f"{TEST_ENTITY_NAME}2", CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SCAN_INTERVAL: 0, @@ -283,7 +289,7 @@ async def test_fan_service_turn(hass, caplog, mock_pymodbus): { CONF_FANS: [ { - CONF_NAME: FAN_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, CONF_VERIFY: {}, diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index b9f6420604f..9400dd56641 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -42,10 +42,14 @@ from homeassistant.components.modbus.const import ( CONF_INPUT_TYPE, CONF_MSG_WAIT, CONF_PARITY, + CONF_RTUOVERTCP, + CONF_SERIAL, CONF_STOPBITS, CONF_SWAP, CONF_SWAP_BYTE, CONF_SWAP_WORD, + CONF_TCP, + CONF_UDP, DATA_TYPE_CUSTOM, DATA_TYPE_INT, DATA_TYPE_STRING, @@ -79,15 +83,17 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .conftest import ReadResult +from .conftest import ( + TEST_ENTITY_NAME, + TEST_MODBUS_HOST, + TEST_MODBUS_NAME, + TEST_PORT_SERIAL, + TEST_PORT_TCP, + ReadResult, +) from tests.common import async_fire_time_changed -TEST_SENSOR_NAME = "testSensor" -TEST_ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_SENSOR_NAME}" -TEST_HOST = "modbusTestHost" -TEST_MODBUS_NAME = "modbusTest" - @pytest.fixture async def mock_modbus_with_pymodbus(hass, caplog, do_config, mock_pymodbus): @@ -128,17 +134,17 @@ async def test_number_validator(): "do_config", [ { - CONF_NAME: TEST_SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 2, CONF_DATA_TYPE: DATA_TYPE_STRING, }, { - CONF_NAME: TEST_SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 2, CONF_DATA_TYPE: DATA_TYPE_INT, }, { - CONF_NAME: TEST_SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 2, CONF_DATA_TYPE: DATA_TYPE_INT, CONF_SWAP: CONF_SWAP_BYTE, @@ -157,29 +163,29 @@ async def test_ok_struct_validator(do_config): "do_config", [ { - CONF_NAME: TEST_SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 8, CONF_DATA_TYPE: DATA_TYPE_INT, }, { - CONF_NAME: TEST_SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 8, CONF_DATA_TYPE: DATA_TYPE_CUSTOM, }, { - CONF_NAME: TEST_SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 8, CONF_DATA_TYPE: DATA_TYPE_CUSTOM, CONF_STRUCTURE: "no good", }, { - CONF_NAME: TEST_SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 20, CONF_DATA_TYPE: DATA_TYPE_CUSTOM, CONF_STRUCTURE: ">f", }, { - CONF_NAME: TEST_SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 1, CONF_DATA_TYPE: DATA_TYPE_CUSTOM, CONF_STRUCTURE: ">f", @@ -200,60 +206,60 @@ async def test_exception_struct_validator(do_config): "do_config", [ { - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: CONF_TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, }, { - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: CONF_TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, CONF_TIMEOUT: 30, CONF_DELAY: 10, }, { - CONF_TYPE: "udp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: CONF_UDP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, }, { - CONF_TYPE: "udp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: CONF_UDP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, CONF_TIMEOUT: 30, CONF_DELAY: 10, }, { - CONF_TYPE: "rtuovertcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: CONF_RTUOVERTCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, }, { - CONF_TYPE: "rtuovertcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: CONF_RTUOVERTCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, CONF_TIMEOUT: 30, CONF_DELAY: 10, }, { - CONF_TYPE: "serial", + CONF_TYPE: CONF_SERIAL, CONF_BAUDRATE: 9600, CONF_BYTESIZE: 8, CONF_METHOD: "rtu", - CONF_PORT: "usb01", + CONF_PORT: TEST_PORT_SERIAL, CONF_PARITY: "E", CONF_STOPBITS: 1, CONF_MSG_WAIT: 100, }, { - CONF_TYPE: "serial", + CONF_TYPE: CONF_SERIAL, CONF_BAUDRATE: 9600, CONF_BYTESIZE: 8, CONF_METHOD: "rtu", - CONF_PORT: "usb01", + CONF_PORT: TEST_PORT_SERIAL, CONF_PARITY: "E", CONF_STOPBITS: 1, CONF_NAME: TEST_MODBUS_NAME, @@ -261,43 +267,43 @@ async def test_exception_struct_validator(do_config): CONF_DELAY: 10, }, { - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: CONF_TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_DELAY: 5, }, [ { - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: CONF_TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, }, { - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, - CONF_NAME: TEST_MODBUS_NAME + "2", + CONF_TYPE: CONF_TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, + CONF_NAME: f"{TEST_MODBUS_NAME}2", }, { - CONF_TYPE: "serial", + CONF_TYPE: CONF_SERIAL, CONF_BAUDRATE: 9600, CONF_BYTESIZE: 8, CONF_METHOD: "rtu", - CONF_PORT: "usb01", + CONF_PORT: TEST_PORT_SERIAL, CONF_PARITY: "E", CONF_STOPBITS: 1, - CONF_NAME: TEST_MODBUS_NAME + "3", + CONF_NAME: f"{TEST_MODBUS_NAME}3", }, ], { # Special test for scan_interval validator with scan_interval: 0 - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: CONF_TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_SENSORS: [ { - CONF_NAME: TEST_SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 117, CONF_SCAN_INTERVAL: 0, } @@ -320,11 +326,11 @@ SERVICE = "service" [ { CONF_NAME: TEST_MODBUS_NAME, - CONF_TYPE: "serial", + CONF_TYPE: CONF_SERIAL, CONF_BAUDRATE: 9600, CONF_BYTESIZE: 8, CONF_METHOD: "rtu", - CONF_PORT: "usb01", + CONF_PORT: TEST_PORT_SERIAL, CONF_PARITY: "E", CONF_STOPBITS: 1, }, @@ -425,14 +431,14 @@ async def mock_modbus_read_pymodbus( config = { DOMAIN: [ { - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: CONF_TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, do_group: [ { CONF_INPUT_TYPE: do_type, - CONF_NAME: TEST_SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_SCAN_INTERVAL: do_scan_interval, } @@ -482,7 +488,7 @@ async def test_pb_read( """Run test for different read.""" # Check state - entity_id = f"{do_domain}.{TEST_SENSOR_NAME}" + entity_id = f"{do_domain}.{TEST_ENTITY_NAME}" state = hass.states.get(entity_id).state assert hass.states.get(entity_id).state @@ -499,9 +505,9 @@ async def test_pymodbus_constructor_fail(hass, caplog): config = { DOMAIN: [ { - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: CONF_TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, } ] } @@ -522,9 +528,9 @@ async def test_pymodbus_close_fail(hass, caplog, mock_pymodbus): config = { DOMAIN: [ { - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: CONF_TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, } ] } @@ -543,19 +549,19 @@ async def test_delay(hass, mock_pymodbus): # We "hijiack" a binary_sensor to make a proper blackbox test. test_delay = 15 test_scan_interval = 5 - entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}" + entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_ENTITY_NAME}" config = { DOMAIN: [ { - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: CONF_TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, CONF_DELAY: test_delay, CONF_BINARY_SENSORS: [ { CONF_INPUT_TYPE: CALL_TYPE_COIL, - CONF_NAME: f"{TEST_SENSOR_NAME}", + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 52, CONF_SCAN_INTERVAL: test_scan_interval, }, diff --git a/tests/components/modbus/test_light.py b/tests/components/modbus/test_light.py index 3b3966cdf8a..49bfed3e19a 100644 --- a/tests/components/modbus/test_light.py +++ b/tests/components/modbus/test_light.py @@ -11,6 +11,7 @@ from homeassistant.components.modbus.const import ( CONF_INPUT_TYPE, CONF_STATE_OFF, CONF_STATE_ON, + CONF_TCP, CONF_VERIFY, CONF_WRITE_TYPE, MODBUS_DOMAIN, @@ -33,10 +34,15 @@ from homeassistant.const import ( from homeassistant.core import State from homeassistant.setup import async_setup_component -from .conftest import ReadResult, base_test +from .conftest import ( + TEST_ENTITY_NAME, + TEST_MODBUS_HOST, + TEST_PORT_TCP, + ReadResult, + base_test, +) -LIGHT_NAME = "test_light" -ENTITY_ID = f"{LIGHT_DOMAIN}.{LIGHT_NAME}" +ENTITY_ID = f"{LIGHT_DOMAIN}.{TEST_ENTITY_NAME}" @pytest.mark.parametrize( @@ -45,7 +51,7 @@ ENTITY_ID = f"{LIGHT_DOMAIN}.{LIGHT_NAME}" { CONF_LIGHTS: [ { - CONF_NAME: LIGHT_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, } ] @@ -53,7 +59,7 @@ ENTITY_ID = f"{LIGHT_DOMAIN}.{LIGHT_NAME}" { CONF_LIGHTS: [ { - CONF_NAME: LIGHT_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, } @@ -62,7 +68,7 @@ ENTITY_ID = f"{LIGHT_DOMAIN}.{LIGHT_NAME}" { CONF_LIGHTS: [ { - CONF_NAME: LIGHT_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -79,7 +85,7 @@ ENTITY_ID = f"{LIGHT_DOMAIN}.{LIGHT_NAME}" { CONF_LIGHTS: [ { - CONF_NAME: LIGHT_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -96,7 +102,7 @@ ENTITY_ID = f"{LIGHT_DOMAIN}.{LIGHT_NAME}" { CONF_LIGHTS: [ { - CONF_NAME: LIGHT_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -113,7 +119,7 @@ ENTITY_ID = f"{LIGHT_DOMAIN}.{LIGHT_NAME}" { CONF_LIGHTS: [ { - CONF_NAME: LIGHT_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -165,13 +171,13 @@ async def test_all_light(hass, call_type, regs, verify, expected): state = await base_test( hass, { - CONF_NAME: LIGHT_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_WRITE_TYPE: call_type, **verify, }, - LIGHT_NAME, + TEST_ENTITY_NAME, LIGHT_DOMAIN, CONF_LIGHTS, None, @@ -194,7 +200,7 @@ async def test_all_light(hass, call_type, regs, verify, expected): { CONF_LIGHTS: [ { - CONF_NAME: LIGHT_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SCAN_INTERVAL: 0, } @@ -213,18 +219,18 @@ async def test_light_service_turn(hass, caplog, mock_pymodbus): ENTITY_ID2 = f"{ENTITY_ID}2" config = { MODBUS_DOMAIN: { - CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", - CONF_PORT: 5501, + CONF_TYPE: CONF_TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_LIGHTS: [ { - CONF_NAME: LIGHT_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SCAN_INTERVAL: 0, }, { - CONF_NAME: f"{LIGHT_NAME}2", + CONF_NAME: f"{TEST_ENTITY_NAME}2", CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SCAN_INTERVAL: 0, @@ -283,7 +289,7 @@ async def test_light_service_turn(hass, caplog, mock_pymodbus): { CONF_LIGHTS: [ { - CONF_NAME: LIGHT_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, CONF_VERIFY: {}, diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index ef784c9edb6..e69a6be41a4 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -35,10 +35,9 @@ from homeassistant.const import ( ) from homeassistant.core import State -from .conftest import ReadResult, base_test +from .conftest import TEST_ENTITY_NAME, ReadResult, base_test -SENSOR_NAME = "test_sensor" -ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" +ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}" @pytest.mark.parametrize( @@ -47,7 +46,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, } ] @@ -55,7 +54,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_SLAVE: 10, CONF_COUNT: 1, @@ -71,7 +70,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_SLAVE: 10, CONF_COUNT: 1, @@ -87,7 +86,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_COUNT: 1, CONF_SWAP: CONF_SWAP_NONE, @@ -97,7 +96,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_COUNT: 1, CONF_SWAP: CONF_SWAP_BYTE, @@ -107,7 +106,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_COUNT: 2, CONF_SWAP: CONF_SWAP_WORD, @@ -117,7 +116,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_COUNT: 2, CONF_SWAP: CONF_SWAP_WORD_BYTE, @@ -139,7 +138,7 @@ async def test_config_sensor(hass, mock_modbus): { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_COUNT: 8, CONF_PRECISION: 2, @@ -154,7 +153,7 @@ async def test_config_sensor(hass, mock_modbus): { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_COUNT: 2, CONF_PRECISION: 2, @@ -169,7 +168,7 @@ async def test_config_sensor(hass, mock_modbus): { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_DATA_TYPE: DATA_TYPE_CUSTOM, CONF_COUNT: 4, @@ -184,7 +183,7 @@ async def test_config_sensor(hass, mock_modbus): { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_DATA_TYPE: DATA_TYPE_CUSTOM, CONF_COUNT: 4, @@ -193,13 +192,13 @@ async def test_config_sensor(hass, mock_modbus): }, ] }, - "Error in sensor test_sensor. The `structure` field can not be empty", + f"Error in sensor {TEST_ENTITY_NAME}. The `structure` field can not be empty", ), ( { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_DATA_TYPE: DATA_TYPE_CUSTOM, CONF_COUNT: 4, @@ -214,7 +213,7 @@ async def test_config_sensor(hass, mock_modbus): { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_DATA_TYPE: DATA_TYPE_CUSTOM, CONF_COUNT: 1, @@ -223,7 +222,7 @@ async def test_config_sensor(hass, mock_modbus): }, ] }, - "Error in sensor test_sensor swap(word) not possible due to the registers count: 1, needed: 2", + f"Error in sensor {TEST_ENTITY_NAME} swap(word) not possible due to the registers count: 1, needed: 2", ), ], ) @@ -508,8 +507,8 @@ async def test_all_sensor(hass, cfg, regs, expected): state = await base_test( hass, - {CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 1234, **cfg}, - SENSOR_NAME, + {CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, **cfg}, + TEST_ENTITY_NAME, SENSOR_DOMAIN, CONF_SENSORS, CONF_REGISTERS, @@ -562,8 +561,8 @@ async def test_struct_sensor(hass, cfg, regs, expected): state = await base_test( hass, - {CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 1234, **cfg}, - SENSOR_NAME, + {CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, **cfg}, + TEST_ENTITY_NAME, SENSOR_DOMAIN, CONF_SENSORS, None, @@ -586,7 +585,7 @@ async def test_struct_sensor(hass, cfg, regs, expected): { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_SCAN_INTERVAL: 0, } @@ -605,7 +604,7 @@ async def test_restore_state_sensor(hass, mock_test_state, mock_modbus): { CONF_SENSORS: [ { - CONF_NAME: SENSOR_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, } diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py index 48c8ca9e15f..3838e7a95d5 100644 --- a/tests/components/modbus/test_switch.py +++ b/tests/components/modbus/test_switch.py @@ -13,6 +13,7 @@ from homeassistant.components.modbus.const import ( CONF_INPUT_TYPE, CONF_STATE_OFF, CONF_STATE_ON, + CONF_TCP, CONF_VERIFY, CONF_WRITE_TYPE, MODBUS_DOMAIN, @@ -39,12 +40,17 @@ from homeassistant.core import State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .conftest import ReadResult, base_test +from .conftest import ( + TEST_ENTITY_NAME, + TEST_MODBUS_HOST, + TEST_PORT_TCP, + ReadResult, + base_test, +) from tests.common import async_fire_time_changed -SWITCH_NAME = "test_switch" -ENTITY_ID = f"{SWITCH_DOMAIN}.{SWITCH_NAME}" +ENTITY_ID = f"{SWITCH_DOMAIN}.{TEST_ENTITY_NAME}" @pytest.mark.parametrize( @@ -53,7 +59,7 @@ ENTITY_ID = f"{SWITCH_DOMAIN}.{SWITCH_NAME}" { CONF_SWITCHES: [ { - CONF_NAME: SWITCH_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, } ] @@ -61,7 +67,7 @@ ENTITY_ID = f"{SWITCH_DOMAIN}.{SWITCH_NAME}" { CONF_SWITCHES: [ { - CONF_NAME: SWITCH_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, } @@ -70,7 +76,7 @@ ENTITY_ID = f"{SWITCH_DOMAIN}.{SWITCH_NAME}" { CONF_SWITCHES: [ { - CONF_NAME: SWITCH_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -88,7 +94,7 @@ ENTITY_ID = f"{SWITCH_DOMAIN}.{SWITCH_NAME}" { CONF_SWITCHES: [ { - CONF_NAME: SWITCH_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -107,7 +113,7 @@ ENTITY_ID = f"{SWITCH_DOMAIN}.{SWITCH_NAME}" { CONF_SWITCHES: [ { - CONF_NAME: SWITCH_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -125,7 +131,7 @@ ENTITY_ID = f"{SWITCH_DOMAIN}.{SWITCH_NAME}" { CONF_SWITCHES: [ { - CONF_NAME: SWITCH_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -179,13 +185,13 @@ async def test_all_switch(hass, call_type, regs, verify, expected): state = await base_test( hass, { - CONF_NAME: SWITCH_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_WRITE_TYPE: call_type, **verify, }, - SWITCH_NAME, + TEST_ENTITY_NAME, SWITCH_DOMAIN, CONF_SWITCHES, None, @@ -208,7 +214,7 @@ async def test_all_switch(hass, call_type, regs, verify, expected): { CONF_SWITCHES: [ { - CONF_NAME: SWITCH_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SCAN_INTERVAL: 0, } @@ -224,21 +230,21 @@ async def test_restore_state_switch(hass, mock_test_state, mock_modbus): async def test_switch_service_turn(hass, caplog, mock_pymodbus): """Run test for service turn_on/turn_off.""" - ENTITY_ID2 = f"{SWITCH_DOMAIN}.{SWITCH_NAME}2" + ENTITY_ID2 = f"{SWITCH_DOMAIN}.{TEST_ENTITY_NAME}2" config = { MODBUS_DOMAIN: { - CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", - CONF_PORT: 5501, + CONF_TYPE: CONF_TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_SWITCHES: [ { - CONF_NAME: SWITCH_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SCAN_INTERVAL: 0, }, { - CONF_NAME: f"{SWITCH_NAME}2", + CONF_NAME: f"{TEST_ENTITY_NAME}2", CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SCAN_INTERVAL: 0, @@ -297,7 +303,7 @@ async def test_switch_service_turn(hass, caplog, mock_pymodbus): { CONF_SWITCHES: [ { - CONF_NAME: SWITCH_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, CONF_VERIFY: {}, @@ -324,12 +330,12 @@ async def test_delay_switch(hass, mock_pymodbus): config = { MODBUS_DOMAIN: [ { - CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", - CONF_PORT: 5501, + CONF_TYPE: CONF_TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, CONF_SWITCHES: [ { - CONF_NAME: SWITCH_NAME, + CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_SCAN_INTERVAL: 0, CONF_VERIFY: { From 909af30c7c5ddc6ada0be2aefb6f942dee25f2b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 17 Aug 2021 22:04:05 +0200 Subject: [PATCH 282/355] Tractive, update library (#54775) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tractive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tractive/manifest.json b/homeassistant/components/tractive/manifest.json index 2328c07f905..73ee75a4ac5 100644 --- a/homeassistant/components/tractive/manifest.json +++ b/homeassistant/components/tractive/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tractive", "requirements": [ - "aiotractive==0.5.1" + "aiotractive==0.5.2" ], "codeowners": [ "@Danielhiversen", diff --git a/requirements_all.txt b/requirements_all.txt index 5a535b86c13..a764e7721bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -246,7 +246,7 @@ aioswitcher==2.0.4 aiosyncthing==0.5.1 # homeassistant.components.tractive -aiotractive==0.5.1 +aiotractive==0.5.2 # homeassistant.components.unifi aiounifi==26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 16591436f25..5ba9adafa97 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -167,7 +167,7 @@ aioswitcher==2.0.4 aiosyncthing==0.5.1 # homeassistant.components.tractive -aiotractive==0.5.1 +aiotractive==0.5.2 # homeassistant.components.unifi aiounifi==26 From 4ef04898e92fb78ca8b2f1b4b4fdce43f9b3d031 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 17 Aug 2021 16:38:20 -0400 Subject: [PATCH 283/355] Fix goalzero sensor not using SensorEntity class (#54773) --- homeassistant/components/goalzero/sensor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py index 31eadd55969..dbb85aa2d48 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -1,7 +1,11 @@ """Support for Goal Zero Yeti Sensors.""" from __future__ import annotations -from homeassistant.components.sensor import ATTR_LAST_RESET, ATTR_STATE_CLASS +from homeassistant.components.sensor import ( + ATTR_LAST_RESET, + ATTR_STATE_CLASS, + SensorEntity, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_NAME, @@ -36,7 +40,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(sensors, True) -class YetiSensor(YetiEntity): +class YetiSensor(YetiEntity, SensorEntity): """Representation of a Goal Zero Yeti sensor.""" def __init__(self, api, coordinator, name, sensor_name, server_unique_id): From 8eec9498358719718d1ef520a021d8ab30f985f8 Mon Sep 17 00:00:00 2001 From: gjong Date: Tue, 17 Aug 2021 22:45:14 +0200 Subject: [PATCH 284/355] Fix connectivity issue in the Youless integration (#54764) --- 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 d00f0457b85..1ea7bc67ba9 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.10"], + "requirements": ["youless-api==0.12"], "codeowners": ["@gjong"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index a764e7721bd..efb249aac12 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2434,7 +2434,7 @@ yeelight==0.7.2 yeelightsunflower==0.0.10 # homeassistant.components.youless -youless-api==0.10 +youless-api==0.12 # homeassistant.components.media_extractor youtube_dl==2021.04.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5ba9adafa97..7cf5441182e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1351,7 +1351,7 @@ yalexs==1.1.13 yeelight==0.7.2 # homeassistant.components.youless -youless-api==0.10 +youless-api==0.12 # homeassistant.components.onvif zeep[async]==4.0.0 From 3a78f1fce6cbb6b41c465536a4991aa6feff7d02 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 17 Aug 2021 23:05:31 +0200 Subject: [PATCH 285/355] Force STATE_CLASS_TOTAL_INCREASING to reset to 0 (#54751) * Force STATE_CLASS_TOTAL_INCREASING to reset to 0 * Tweak * Correct detection of new cycle * Fix typing --- homeassistant/components/sensor/recorder.py | 8 ++++++-- tests/components/sensor/test_recorder.py | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 66366934d27..48f80bab5c2 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -308,7 +308,7 @@ def compile_statistics( elif old_state is None and last_reset is None: reset = True elif state_class == STATE_CLASS_TOTAL_INCREASING and ( - old_state is None or fstate < old_state + old_state is None or (new_state is not None and fstate < new_state) ): reset = True @@ -319,7 +319,11 @@ def compile_statistics( # ..and update the starting point new_state = fstate old_last_reset = last_reset - old_state = new_state + # Force a new cycle for STATE_CLASS_TOTAL_INCREASING to start at 0 + if state_class == STATE_CLASS_TOTAL_INCREASING and old_state: + old_state = 0 + else: + old_state = new_state else: new_state = fstate diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 45d81e4b678..d4dee872823 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -383,7 +383,7 @@ def test_compile_hourly_sum_statistics_total_increasing( "min": None, "last_reset": None, "state": approx(factor * seq[5]), - "sum": approx(factor * 40.0), + "sum": approx(factor * 50.0), }, { "statistic_id": "sensor.test1", @@ -393,7 +393,7 @@ def test_compile_hourly_sum_statistics_total_increasing( "min": None, "last_reset": None, "state": approx(factor * seq[8]), - "sum": approx(factor * 70.0), + "sum": approx(factor * 80.0), }, ] } From 6da83b90f7a993f584faeb1af8fc7164320bbea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 18 Aug 2021 00:46:48 +0200 Subject: [PATCH 286/355] Rfxtrx,STATE_CLASS_TOTAL_INCREASING (#54776) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/rfxtrx/sensor.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 49a8bbb974c..7ce986d7082 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, ) @@ -38,7 +39,6 @@ from homeassistant.const import ( UV_INDEX, ) from homeassistant.core import callback -from homeassistant.util import dt from . import ( CONF_DATA_BITS, @@ -145,8 +145,7 @@ SENSOR_TYPES = ( RfxtrxSensorEntityDescription( key="Total usage", device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset=dt.utc_from_timestamp(0), + state_class=STATE_CLASS_TOTAL_INCREASING, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), RfxtrxSensorEntityDescription( @@ -173,14 +172,12 @@ SENSOR_TYPES = ( ), RfxtrxSensorEntityDescription( key="Count", - state_class=STATE_CLASS_MEASUREMENT, - last_reset=dt.utc_from_timestamp(0), + state_class=STATE_CLASS_TOTAL_INCREASING, native_unit_of_measurement="count", ), RfxtrxSensorEntityDescription( key="Counter value", - state_class=STATE_CLASS_MEASUREMENT, - last_reset=dt.utc_from_timestamp(0), + state_class=STATE_CLASS_TOTAL_INCREASING, native_unit_of_measurement="count", ), RfxtrxSensorEntityDescription( From 67e9035e4ea06dc86abbffb17e1035504c4e2648 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Tue, 17 Aug 2021 16:56:33 -0600 Subject: [PATCH 287/355] Improve myq error handling for opening/closing cover (#54724) --- homeassistant/components/myq/cover.py | 51 ++++++++++----------------- homeassistant/components/myq/light.py | 2 +- 2 files changed, 20 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index 8d36db8e0ab..87b8223c477 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -18,6 +18,7 @@ from homeassistant.components.cover import ( CoverEntity, ) from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, MYQ_TO_HASS @@ -43,14 +44,11 @@ class MyQDevice(CoordinatorEntity, CoverEntity): """Initialize with API object, device id.""" super().__init__(coordinator) self._device = device - - @property - def device_class(self): - """Define this cover as a garage door.""" - device_type = self._device.device_type - if device_type is not None and device_type == MYQ_DEVICE_TYPE_GATE: - return DEVICE_CLASS_GATE - return DEVICE_CLASS_GARAGE + if device.device_type == MYQ_DEVICE_TYPE_GATE: + self._attr_device_class = DEVICE_CLASS_GATE + else: + self._attr_device_class = DEVICE_CLASS_GARAGE + self._attr_unique_id = device.device_id @property def name(self): @@ -60,11 +58,8 @@ class MyQDevice(CoordinatorEntity, CoverEntity): @property def available(self): """Return if the device is online.""" - if not self.coordinator.last_update_success: - return False - # Not all devices report online so assume True if its missing - return self._device.device_json[MYQ_DEVICE_STATE].get( + return super().available and self._device.device_json[MYQ_DEVICE_STATE].get( MYQ_DEVICE_STATE_ONLINE, True ) @@ -93,11 +88,6 @@ class MyQDevice(CoordinatorEntity, CoverEntity): """Flag supported features.""" return SUPPORT_OPEN | SUPPORT_CLOSE - @property - def unique_id(self): - """Return a unique, Home Assistant friendly identifier for this entity.""" - return self._device.device_id - async def async_close_cover(self, **kwargs): """Issue close command to cover.""" if self.is_closing or self.is_closed: @@ -106,23 +96,21 @@ class MyQDevice(CoordinatorEntity, CoverEntity): try: wait_task = await self._device.close(wait_for_state=False) except MyQError as err: - _LOGGER.error( - "Closing of cover %s failed with error: %s", self._device.name, str(err) - ) - - return + raise HomeAssistantError( + f"Closing of cover {self._device.name} failed with error: {err}" + ) from err # Write closing state to HASS self.async_write_ha_state() result = wait_task if isinstance(wait_task, bool) else await wait_task - if not result: - _LOGGER.error("Closing of cover %s failed", self._device.name) - # Write final state to HASS self.async_write_ha_state() + if not result: + raise HomeAssistantError(f"Closing of cover {self._device.name} failed") + async def async_open_cover(self, **kwargs): """Issue open command to cover.""" if self.is_opening or self.is_open: @@ -131,22 +119,21 @@ class MyQDevice(CoordinatorEntity, CoverEntity): try: wait_task = await self._device.open(wait_for_state=False) except MyQError as err: - _LOGGER.error( - "Opening of cover %s failed with error: %s", self._device.name, str(err) - ) - return + raise HomeAssistantError( + f"Opening of cover {self._device.name} failed with error: {err}" + ) from err # Write opening state to HASS self.async_write_ha_state() result = wait_task if isinstance(wait_task, bool) else await wait_task - if not result: - _LOGGER.error("Opening of cover %s failed", self._device.name) - # Write final state to HASS self.async_write_ha_state() + if not result: + raise HomeAssistantError(f"Opening of cover {self._device.name} failed") + @property def device_info(self): """Return the device_info of the device.""" diff --git a/homeassistant/components/myq/light.py b/homeassistant/components/myq/light.py index f26d28fe3a3..98119c2157a 100644 --- a/homeassistant/components/myq/light.py +++ b/homeassistant/components/myq/light.py @@ -90,7 +90,7 @@ class MyQLight(CoordinatorEntity, LightEntity): f"Turning light {self._device.name} off failed with error: {err}" ) from err - # Write opening state to HASS + # Write new state to HASS self.async_write_ha_state() @property From 3bc45eacfc14e52815190ef9cc7e2a3f8d160e29 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 18 Aug 2021 01:29:40 +0200 Subject: [PATCH 288/355] Fix tplink doing I/O in event loop and optimize (#54570) * Optimize tplink i/o * Cache has_emeter reduceing the number of i/o requests on hs300 by 5 * Use the state from the sysinfo dict for non-strips reducing required requests by one * Remove I/O from __init__, read has_emeter from sysinfo * Cleanup __init__ to avoid I/O * Re-use the sysinfo response for has_emeter * Use async_add_executor_job() to execute the synchronous I/O ops. * use the device alias instead of host for coordinator, use executor for unavailable_devices * Remove unnecessary self.hass assignment Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/tplink/__init__.py | 26 ++++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 5c69247eea8..aad934b2600 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -154,7 +154,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for device in unavailable_devices: try: - device.get_sysinfo() + await hass.async_add_executor_job(device.get_sysinfo) except SmartDeviceException: continue _LOGGER.debug( @@ -168,7 +168,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for switch in switches: try: - await hass.async_add_executor_job(switch.get_sysinfo) + info = await hass.async_add_executor_job(switch.get_sysinfo) except SmartDeviceException: _LOGGER.warning( "Device at '%s' not reachable during setup, will retry later", @@ -179,7 +179,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass_data[COORDINATORS][ switch.context or switch.mac - ] = coordinator = SmartPlugDataUpdateCoordinator(hass, switch) + ] = coordinator = SmartPlugDataUpdateCoordinator(hass, switch, info["alias"]) await coordinator.async_config_entry_first_refresh() if unavailable_devices: @@ -215,16 +215,20 @@ class SmartPlugDataUpdateCoordinator(DataUpdateCoordinator): self, hass: HomeAssistant, smartplug: SmartPlug, + alias: str, ) -> None: """Initialize DataUpdateCoordinator to gather data for specific SmartPlug.""" self.smartplug = smartplug update_interval = timedelta(seconds=30) super().__init__( - hass, _LOGGER, name=smartplug.alias, update_interval=update_interval + hass, + _LOGGER, + name=alias, + update_interval=update_interval, ) - async def _async_update_data(self) -> dict: + def _update_data(self) -> dict: """Fetch all device and sensor data from api.""" try: info = self.smartplug.sys_info @@ -237,9 +241,7 @@ class SmartPlugDataUpdateCoordinator(DataUpdateCoordinator): if self.smartplug.context is None: data[CONF_ALIAS] = info["alias"] data[CONF_DEVICE_ID] = info["mac"] - data[CONF_STATE] = ( - self.smartplug.state == self.smartplug.SWITCH_STATE_ON - ) + data[CONF_STATE] = bool(info["relay_state"]) else: plug_from_context = next( c @@ -249,7 +251,9 @@ class SmartPlugDataUpdateCoordinator(DataUpdateCoordinator): data[CONF_ALIAS] = plug_from_context["alias"] data[CONF_DEVICE_ID] = self.smartplug.context data[CONF_STATE] = plug_from_context["state"] == 1 - if self.smartplug.has_emeter: + + # Check if the device has emeter + if "ENE" in info["feature"]: emeter_readings = self.smartplug.get_emeter_realtime() data[CONF_EMETER_PARAMS] = { ATTR_CURRENT_POWER_W: round(float(emeter_readings["power"]), 2), @@ -270,3 +274,7 @@ class SmartPlugDataUpdateCoordinator(DataUpdateCoordinator): self.name = data[CONF_ALIAS] return data + + async def _async_update_data(self) -> dict: + """Fetch all device and sensor data from api.""" + return await self.hass.async_add_executor_job(self._update_data) From b981e69f951afaae4b45543b0fc78866aedef6ca Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 18 Aug 2021 02:00:10 +0200 Subject: [PATCH 289/355] Update SolarEdge to use new state classes (#54731) --- homeassistant/components/solaredge/const.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py index 06d8813130e..c9c7136fb94 100644 --- a/homeassistant/components/solaredge/const.py +++ b/homeassistant/components/solaredge/const.py @@ -2,7 +2,10 @@ from datetime import timedelta import logging -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, +) from homeassistant.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, @@ -10,7 +13,6 @@ from homeassistant.const import ( PERCENTAGE, POWER_WATT, ) -from homeassistant.util import dt as dt_util from .models import SolarEdgeSensorEntityDescription @@ -40,8 +42,7 @@ SENSOR_TYPES = [ json_key="lifeTimeData", name="Lifetime energy", icon="mdi:solar-power", - last_reset=dt_util.utc_from_timestamp(0), - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), From 0100ffcb8c5b91bc1f8a5eb7a3366bcec9fcc415 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 18 Aug 2021 00:13:44 +0000 Subject: [PATCH 290/355] [ci skip] Translation update --- .../components/airtouch4/translations/ca.json | 19 +++++++++++++ .../components/airtouch4/translations/en.json | 28 ++++++++++--------- .../components/airtouch4/translations/et.json | 19 +++++++++++++ .../components/airtouch4/translations/ru.json | 19 +++++++++++++ .../binary_sensor/translations/ca.json | 8 ++++++ .../binary_sensor/translations/cs.json | 8 ++++++ .../binary_sensor/translations/de.json | 8 ++++++ .../binary_sensor/translations/et.json | 8 ++++++ .../binary_sensor/translations/fr.json | 8 ++++++ .../binary_sensor/translations/hu.json | 8 ++++++ .../binary_sensor/translations/no.json | 8 ++++++ .../binary_sensor/translations/ru.json | 8 ++++++ .../binary_sensor/translations/zh-Hant.json | 8 ++++++ .../components/ifttt/translations/de.json | 2 +- .../components/mutesync/translations/de.json | 2 +- .../components/risco/translations/de.json | 2 +- .../components/sensor/translations/ca.json | 16 +++++++++++ .../components/sensor/translations/de.json | 16 +++++++++++ .../components/sensor/translations/et.json | 16 +++++++++++ .../components/sensor/translations/hu.json | 16 +++++++++++ .../components/sensor/translations/no.json | 16 +++++++++++ .../components/sensor/translations/ru.json | 16 +++++++++++ .../sensor/translations/zh-Hant.json | 16 +++++++++++ .../components/tractive/translations/ca.json | 4 ++- .../components/tractive/translations/cs.json | 3 +- .../components/tractive/translations/de.json | 1 + .../components/tractive/translations/et.json | 4 ++- .../components/tractive/translations/no.json | 4 ++- .../components/tractive/translations/ru.json | 4 ++- .../xiaomi_aqara/translations/de.json | 2 +- .../xiaomi_miio/translations/ca.json | 2 +- .../xiaomi_miio/translations/de.json | 2 +- .../xiaomi_miio/translations/et.json | 2 +- 33 files changed, 278 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/airtouch4/translations/ca.json create mode 100644 homeassistant/components/airtouch4/translations/et.json create mode 100644 homeassistant/components/airtouch4/translations/ru.json diff --git a/homeassistant/components/airtouch4/translations/ca.json b/homeassistant/components/airtouch4/translations/ca.json new file mode 100644 index 00000000000..083c4a0ba87 --- /dev/null +++ b/homeassistant/components/airtouch4/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "no_units": "No s'han trobat grups AirTouch 4." + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + }, + "title": "Configura els detalls de connexi\u00f3 d'AirTouch 4." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/en.json b/homeassistant/components/airtouch4/translations/en.json index 2bde2ea760a..0f86b787249 100644 --- a/homeassistant/components/airtouch4/translations/en.json +++ b/homeassistant/components/airtouch4/translations/en.json @@ -1,17 +1,19 @@ { "config": { - - "error": { - "cannot_connect": "Failed to connect", - "no_units": "Could not find any AirTouch 4 Groups." - }, - "step": { - "user": { - "title": "Setup your AirTouch 4.", - "data": { - "host": "Host" - } + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "no_units": "Could not find any AirTouch 4 Groups." + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "title": "Setup your AirTouch 4 connection details." + } } - } } - } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/et.json b/homeassistant/components/airtouch4/translations/et.json new file mode 100644 index 00000000000..2b42935b18e --- /dev/null +++ b/homeassistant/components/airtouch4/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "no_units": "Ei leidnud \u00fchtegi AirTouch 4 gruppi." + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "title": "AirTouch 4 \u00fchenduse \u00fcksikasjade seadistamine." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/ru.json b/homeassistant/components/airtouch4/translations/ru.json new file mode 100644 index 00000000000..cbb7b10de79 --- /dev/null +++ b/homeassistant/components/airtouch4/translations/ru.json @@ -0,0 +1,19 @@ +{ + "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.", + "no_units": "\u0413\u0440\u0443\u043f\u043f\u044b AirTouch 4 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "title": "AirTouch 4" + } + } + } +} \ 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 9c92a50246a..089f72f51d5 100644 --- a/homeassistant/components/binary_sensor/translations/ca.json +++ b/homeassistant/components/binary_sensor/translations/ca.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} no est\u00e0 detectant cap problema", "is_no_smoke": "{entity_name} no detecta fum", "is_no_sound": "{entity_name} no detecta so", + "is_no_update": "{entity_name} est\u00e0 actualitzat/da", "is_no_vibration": "{entity_name} no detecta vibraci\u00f3", "is_not_bat_low": "Bateria de {entity_name} normal", "is_not_cold": "{entity_name} no est\u00e0 fred", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name} est\u00e0 detectant fum", "is_sound": "{entity_name} est\u00e0 detectant so", "is_unsafe": "{entity_name} \u00e9s insegur", + "is_update": "{entity_name} t\u00e9 una actualitzaci\u00f3 disponible", "is_vibration": "{entity_name} est\u00e0 detectant vibraci\u00f3" }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name} ha deixat de detectar un problema", "no_smoke": "{entity_name} ha deixat de detectar fum", "no_sound": "{entity_name} ha deixat de detectar so", + "no_update": "{entity_name} s'ha actualitzat", "no_vibration": "{entity_name} ha deixat de detectar vibraci\u00f3", "not_bat_low": "Bateria de {entity_name} normal", "not_cold": "{entity_name} es torna no-fred", @@ -86,6 +89,7 @@ "turned_off": "{entity_name} apagat", "turned_on": "{entity_name} enc\u00e8s", "unsafe": "{entity_name} es torna insegur", + "update": "{entity_name} obt\u00e9 una nova actualitzaci\u00f3 disponible", "vibration": "{entity_name} ha comen\u00e7at a detectar vibraci\u00f3" } }, @@ -178,6 +182,10 @@ "off": "Lliure", "on": "Detectat" }, + "update": { + "off": "Actualitzat/da", + "on": "Actualitzaci\u00f3 disponible" + }, "vibration": { "off": "Lliure", "on": "Detectat" diff --git a/homeassistant/components/binary_sensor/translations/cs.json b/homeassistant/components/binary_sensor/translations/cs.json index 90f25332bdb..25b82e54de7 100644 --- a/homeassistant/components/binary_sensor/translations/cs.json +++ b/homeassistant/components/binary_sensor/translations/cs.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} nehl\u00e1s\u00ed probl\u00e9m", "is_no_smoke": "{entity_name} nedetekuje kou\u0159", "is_no_sound": "{entity_name} nedetekuje zvuk", + "is_no_update": "{entity_name} je aktu\u00e1ln\u00ed", "is_no_vibration": "{entity_name} nedetekuje vibrace", "is_not_bat_low": "{entity_name} baterie v norm\u00e1lu", "is_not_cold": "{entity_name} nen\u00ed studen\u00fd", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name} detekuje kou\u0159", "is_sound": "{entity_name} detekuje zvuk", "is_unsafe": "{entity_name} nen\u00ed bezpe\u010dno", + "is_update": "{entity_name} m\u00e1 k dispozici aktualizaci", "is_vibration": "{entity_name} detekuje vibrace" }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name} p\u0159estalo detekovat probl\u00e9m", "no_smoke": "{entity_name} p\u0159estalo detekovat kou\u0159", "no_sound": "{entity_name} p\u0159estalo detekovat zvuk", + "no_update": "{entity_name} se stalo aktu\u00e1ln\u00ed", "no_vibration": "{entity_name} p\u0159estalo detekovat vibrace", "not_bat_low": "{entity_name} baterie v norm\u00e1lu", "not_cold": "{entity_name} p\u0159estal b\u00fdt studen\u00fd", @@ -86,6 +89,7 @@ "turned_off": "{entity_name} vypnuto", "turned_on": "{entity_name} zapnuto", "unsafe": "{entity_name} hl\u00e1s\u00ed ohro\u017een\u00ed", + "update": "{entity_name} m\u00e1 k dispozici aktualizaci", "vibration": "{entity_name} za\u010dalo detekovat vibrace" } }, @@ -178,6 +182,10 @@ "off": "Ticho", "on": "Zachycen zvuk" }, + "update": { + "off": "Aktu\u00e1ln\u00ed", + "on": "Aktualizace k dispozici" + }, "vibration": { "off": "Klid", "on": "Zji\u0161t\u011bny vibrace" diff --git a/homeassistant/components/binary_sensor/translations/de.json b/homeassistant/components/binary_sensor/translations/de.json index a2ef817bedb..21d1eff1ebf 100644 --- a/homeassistant/components/binary_sensor/translations/de.json +++ b/homeassistant/components/binary_sensor/translations/de.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} erkennt kein Problem", "is_no_smoke": "{entity_name} erkennt keinen Rauch", "is_no_sound": "{entity_name} erkennt keine Ger\u00e4usche", + "is_no_update": "{entity_name} ist aktuell", "is_no_vibration": "{entity_name} erkennt keine Vibrationen", "is_not_bat_low": "{entity_name} Batterie ist normal", "is_not_cold": "{entity_name} ist nicht kalt", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name} hat Rauch detektiert", "is_sound": "{entity_name} hat Ger\u00e4usche detektiert", "is_unsafe": "{entity_name} ist unsicher", + "is_update": "{entity_name} hat ein Update verf\u00fcgbar", "is_vibration": "{entity_name} erkennt Vibrationen." }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name} hat kein Problem mehr erkannt", "no_smoke": "{entity_name} hat keinen Rauch mehr erkannt", "no_sound": "{entity_name} hat keine Ger\u00e4usche mehr erkannt", + "no_update": "{entity_name} wurde auf den neuesten Stand gebracht", "no_vibration": "{entity_name}hat keine Vibrationen mehr erkannt", "not_bat_low": "{entity_name} Batterie normal", "not_cold": "{entity_name} w\u00e4rmte auf", @@ -86,6 +89,7 @@ "turned_off": "{entity_name} ausgeschaltet", "turned_on": "{entity_name} eingeschaltet", "unsafe": "{entity_name} ist unsicher", + "update": "{entity_name} hat ein Update verf\u00fcgbar", "vibration": "{entity_name} detektiert Vibrationen" } }, @@ -178,6 +182,10 @@ "off": "Normal", "on": "Erkannt" }, + "update": { + "off": "Aktuell", + "on": "Update verf\u00fcgbar" + }, "vibration": { "off": "Normal", "on": "Erkannt" diff --git a/homeassistant/components/binary_sensor/translations/et.json b/homeassistant/components/binary_sensor/translations/et.json index 99fbec0b89e..2a0172300c9 100644 --- a/homeassistant/components/binary_sensor/translations/et.json +++ b/homeassistant/components/binary_sensor/translations/et.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} ei leia probleemi", "is_no_smoke": "{entity_name} ei tuvasta suitsu", "is_no_sound": "{entity_name} ei tuvasta heli", + "is_no_update": "{entity_name} on ajakohane", "is_no_vibration": "{entity_name} ei tuvasta vibratsiooni", "is_not_bat_low": "{entity_name} aku on laetud", "is_not_cold": "{entity_name} ei ole k\u00fclm", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name} tuvastab suitsu", "is_sound": "{entity_name} tuvastab heli", "is_unsafe": "{entity_name} on ebaturvaline", + "is_update": "{entity_name} on saadaval uuendus", "is_vibration": "{entity_name} tuvastab vibratsiooni" }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name} l\u00f5petas probleemi tuvastamise", "no_smoke": "{entity_name} l\u00f5petas suitsu tuvastamise", "no_sound": "{entity_name} l\u00f5petas heli tuvastamise", + "no_update": "{entity_name} on uuendatud", "no_vibration": "{entity_name} l\u00f5petas vibratsiooni tuvastamise", "not_bat_low": "{entity_name} aku on laetud", "not_cold": "{entity_name} ei ole enam k\u00fclm", @@ -86,6 +89,7 @@ "turned_off": "{entity_name} l\u00fclitus v\u00e4lja", "turned_on": "{entity_name} l\u00fclitus sisse", "unsafe": "{entity_name} on ebaturvaline", + "update": "{entity_name} sai saadavaloleva uuenduse", "vibration": "{entity_name} registreeris vibratsiooni" } }, @@ -178,6 +182,10 @@ "off": "Puudub", "on": "Tuvastatud" }, + "update": { + "off": "Ajakohane", + "on": "Saadaval on uuendus" + }, "vibration": { "off": "Puudub", "on": "Tuvastatud" diff --git a/homeassistant/components/binary_sensor/translations/fr.json b/homeassistant/components/binary_sensor/translations/fr.json index ede13a68dc9..aa0686c0375 100644 --- a/homeassistant/components/binary_sensor/translations/fr.json +++ b/homeassistant/components/binary_sensor/translations/fr.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} ne d\u00e9tecte pas de probl\u00e8me", "is_no_smoke": "{entity_name} ne d\u00e9tecte pas de fum\u00e9e", "is_no_sound": "{entity_name} ne d\u00e9tecte pas de son", + "is_no_update": "{entity_name} est \u00e0 jour", "is_no_vibration": "{entity_name} ne d\u00e9tecte pas de vibration", "is_not_bat_low": "{entity_name} batterie normale", "is_not_cold": "{entity_name} n'est pas froid", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name} d\u00e9tecte de la fum\u00e9e", "is_sound": "{entity_name} d\u00e9tecte du son", "is_unsafe": "{entity_name} est dangereux", + "is_update": "{entity_name} a une mise \u00e0 jour disponible", "is_vibration": "{entity_name} d\u00e9tecte des vibrations" }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name} a cess\u00e9 de d\u00e9tecter un probl\u00e8me", "no_smoke": "{entity_name} a cess\u00e9 de d\u00e9tecter de la fum\u00e9e", "no_sound": "{entity_name} a cess\u00e9 de d\u00e9tecter du bruit", + "no_update": "{entity_name} a \u00e9t\u00e9 mis \u00e0 jour", "no_vibration": "{entity_name} a cess\u00e9 de d\u00e9tecter des vibrations", "not_bat_low": "{entity_name} batterie normale", "not_cold": "{entity_name} n'est plus froid", @@ -86,6 +89,7 @@ "turned_off": "{entity_name} est d\u00e9sactiv\u00e9", "turned_on": "{entity_name} est activ\u00e9", "unsafe": "{entity_name} est devenu dangereux", + "update": "{entity_name} a une mise \u00e0 jour disponible", "vibration": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter les vibrations" } }, @@ -178,6 +182,10 @@ "off": "Non d\u00e9tect\u00e9", "on": "D\u00e9tect\u00e9" }, + "update": { + "off": "\u00c0 jour", + "on": "Mise \u00e0 jour disponible" + }, "vibration": { "off": "RAS", "on": "D\u00e9tect\u00e9e" diff --git a/homeassistant/components/binary_sensor/translations/hu.json b/homeassistant/components/binary_sensor/translations/hu.json index c4395ca806c..d8befd7ae35 100644 --- a/homeassistant/components/binary_sensor/translations/hu.json +++ b/homeassistant/components/binary_sensor/translations/hu.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} nem \u00e9szlel probl\u00e9m\u00e1t", "is_no_smoke": "{entity_name} nem \u00e9rz\u00e9kel f\u00fcst\u00f6t", "is_no_sound": "{entity_name} nem \u00e9rz\u00e9kel hangot", + "is_no_update": "{entity_name} naprak\u00e9sz", "is_no_vibration": "{entity_name} nem \u00e9rz\u00e9kel rezg\u00e9st", "is_not_bat_low": "{entity_name} akkufesz\u00fclts\u00e9g megfelel\u0151", "is_not_cold": "{entity_name} nem hideg", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name} f\u00fcst\u00f6t \u00e9rz\u00e9kel", "is_sound": "{entity_name} hangot \u00e9rz\u00e9kel", "is_unsafe": "{entity_name} nem biztons\u00e1gos", + "is_update": "{entity_name} egy friss\u00edt\u00e9s \u00e1ll rendelkez\u00e9sre", "is_vibration": "{entity_name} rezg\u00e9st \u00e9rz\u00e9kel" }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name} m\u00e1r nem \u00e9szlel probl\u00e9m\u00e1t", "no_smoke": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel f\u00fcst\u00f6t", "no_sound": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel hangot", + "no_update": "{entity_name} naprak\u00e9sz lett", "no_vibration": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel rezg\u00e9st", "not_bat_low": "{entity_name} akkufesz\u00fclts\u00e9g megfelel\u0151", "not_cold": "{entity_name} m\u00e1r nem hideg", @@ -86,6 +89,7 @@ "turned_off": "{entity_name} ki lett kapcsolva", "turned_on": "{entity_name} be lett kapcsolva", "unsafe": "{entity_name} m\u00e1r nem biztons\u00e1gos", + "update": "{entity_name} el\u00e9rhet\u0151 friss\u00edt\u00e9s", "vibration": "{entity_name} rezg\u00e9st \u00e9rz\u00e9kel" } }, @@ -178,6 +182,10 @@ "off": "Norm\u00e1l", "on": "\u00c9szlelve" }, + "update": { + "off": "Naprak\u00e9sz", + "on": "Friss\u00edt\u00e9s el\u00e9rhet\u0151" + }, "vibration": { "off": "Norm\u00e1l", "on": "\u00c9szlelve" diff --git a/homeassistant/components/binary_sensor/translations/no.json b/homeassistant/components/binary_sensor/translations/no.json index 023fec6cc39..041643f9cc3 100644 --- a/homeassistant/components/binary_sensor/translations/no.json +++ b/homeassistant/components/binary_sensor/translations/no.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} registrerer ikke et problem", "is_no_smoke": "{entity_name} registrerer ikke r\u00f8yk", "is_no_sound": "{entity_name} registrerer ikke lyd", + "is_no_update": "{entity_name} er oppdatert", "is_no_vibration": "{entity_name} registrerer ikke bevegelse", "is_not_bat_low": "{entity_name} batteri er normalt", "is_not_cold": "{entity_name} er ikke kald", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name} registrerer r\u00f8yk", "is_sound": "{entity_name} registrerer lyd", "is_unsafe": "{entity_name} er utrygg", + "is_update": "{entity_name} har en tilgjengelig oppdatering", "is_vibration": "{entity_name} registrerer vibrasjon" }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name} sluttet \u00e5 registrere problem", "no_smoke": "{entity_name} sluttet \u00e5 registrere r\u00f8yk", "no_sound": "{entity_name} sluttet \u00e5 registrere lyd", + "no_update": "{entity_name} ble oppdatert", "no_vibration": "{entity_name} sluttet \u00e5 registrere vibrasjon", "not_bat_low": "{entity_name} batteri normalt", "not_cold": "{entity_name} ble ikke lenger kald", @@ -86,6 +89,7 @@ "turned_off": "{entity_name} sl\u00e5tt av", "turned_on": "{entity_name} sl\u00e5tt p\u00e5", "unsafe": "{entity_name} ble usikker", + "update": "{entity_name} har en oppdatering tilgjengelig", "vibration": "{entity_name} begynte \u00e5 oppdage vibrasjon" } }, @@ -178,6 +182,10 @@ "off": "Klart", "on": "Oppdaget" }, + "update": { + "off": "Oppdatert", + "on": "Oppdatering tilgjengelig" + }, "vibration": { "off": "Klart", "on": "Oppdaget" diff --git a/homeassistant/components/binary_sensor/translations/ru.json b/homeassistant/components/binary_sensor/translations/ru.json index 2db1506b392..c245d2ba15a 100644 --- a/homeassistant/components/binary_sensor/translations/ru.json +++ b/homeassistant/components/binary_sensor/translations/ru.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", "is_no_smoke": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u044b\u043c", "is_no_sound": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0437\u0432\u0443\u043a", + "is_no_update": "{entity_name} \u043d\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f", "is_no_vibration": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e", "is_not_bat_low": "{entity_name} \u0432 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_not_cold": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043e\u0445\u043b\u0430\u0436\u0434\u0435\u043d\u0438\u0435", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u044b\u043c", "is_sound": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0437\u0432\u0443\u043a", "is_unsafe": "{entity_name} \u0432 \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_update": "\u0414\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 {entity_name}", "is_vibration": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e" }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{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\u0431\u043b\u0435\u043c\u0443", "no_smoke": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u044b\u043c", "no_sound": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0437\u0432\u0443\u043a", + "no_update": "{entity_name} \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442\u0441\u044f", "no_vibration": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e", "not_bat_low": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u044b\u0439 \u0437\u0430\u0440\u044f\u0434", "not_cold": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0445\u043b\u0430\u0436\u0434\u0430\u0442\u044c\u0441\u044f", @@ -86,6 +89,7 @@ "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", + "update": "\u0421\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 {entity_name}", "vibration": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e" } }, @@ -178,6 +182,10 @@ "off": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d", "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", + "on": "\u0414\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435" + }, "vibration": { "off": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430", "on": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430" diff --git a/homeassistant/components/binary_sensor/translations/zh-Hant.json b/homeassistant/components/binary_sensor/translations/zh-Hant.json index bf50782743e..4733d4d1dcc 100644 --- a/homeassistant/components/binary_sensor/translations/zh-Hant.json +++ b/homeassistant/components/binary_sensor/translations/zh-Hant.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name}\u672a\u5075\u6e2c\u5230\u554f\u984c", "is_no_smoke": "{entity_name}\u672a\u5075\u6e2c\u5230\u7159\u9727", "is_no_sound": "{entity_name}\u672a\u5075\u6e2c\u5230\u8072\u97f3", + "is_no_update": "{entity_name} \u5df2\u6700\u65b0", "is_no_vibration": "{entity_name}\u672a\u5075\u6e2c\u5230\u9707\u52d5", "is_not_bat_low": "{entity_name}\u96fb\u91cf\u6b63\u5e38", "is_not_cold": "{entity_name}\u4e0d\u51b7", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name}\u6b63\u5075\u6e2c\u5230\u7159\u9727", "is_sound": "{entity_name}\u6b63\u5075\u6e2c\u5230\u8072\u97f3", "is_unsafe": "{entity_name}\u4e0d\u5b89\u5168", + "is_update": "{entity_name} \u6709\u66f4\u65b0", "is_vibration": "{entity_name}\u6b63\u5075\u6e2c\u5230\u9707\u52d5" }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name}\u5df2\u505c\u6b62\u5075\u6e2c\u554f\u984c", "no_smoke": "{entity_name}\u5df2\u505c\u6b62\u5075\u6e2c\u7159\u9727", "no_sound": "{entity_name}\u5df2\u505c\u6b62\u5075\u6e2c\u8072\u97f3", + "no_update": "{entity_name} \u5df2\u6700\u65b0", "no_vibration": "{entity_name}\u5df2\u505c\u6b62\u5075\u6e2c\u9707\u52d5", "not_bat_low": "{entity_name}\u96fb\u91cf\u6b63\u5e38", "not_cold": "{entity_name}\u5df2\u4e0d\u51b7", @@ -86,6 +89,7 @@ "turned_off": "{entity_name}\u5df2\u95dc\u9589", "turned_on": "{entity_name}\u5df2\u958b\u555f", "unsafe": "{entity_name}\u5df2\u4e0d\u5b89\u5168", + "update": "{entity_name} \u6709\u66f4\u65b0", "vibration": "{entity_name}\u5df2\u5075\u6e2c\u5230\u9707\u52d5" } }, @@ -178,6 +182,10 @@ "off": "\u672a\u89f8\u767c", "on": "\u5df2\u89f8\u767c" }, + "update": { + "off": "\u5df2\u6700\u65b0", + "on": "\u6709\u66f4\u65b0" + }, "vibration": { "off": "\u672a\u5075\u6e2c", "on": "\u5075\u6e2c" diff --git a/homeassistant/components/ifttt/translations/de.json b/homeassistant/components/ifttt/translations/de.json index 5184e89f29a..216511c62f5 100644 --- a/homeassistant/components/ifttt/translations/de.json +++ b/homeassistant/components/ifttt/translations/de.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." }, "create_entry": { - "default": "Um Ereignisse an Home Assistant zu senden, musst du die Aktion \"Eine Webanforderung erstellen\" aus dem [IFTTT Webhook Applet]({applet_url}) ausw\u00e4hlen.\n\nF\u00fclle folgende Informationen aus: \n- URL: `{webhook_url}`\n- Methode: POST\n- Inhaltstyp: application/json\n\nIn der Dokumentation ({docs_url}) findest du Informationen zur Konfiguration der Automation eingehender Daten." + "default": "Um Ereignisse an Home Assistant zu senden, musst du die Aktion \"Eine Webanforderung erstellen\" aus dem [IFTTT Webhook Applet]({applet_url}) ausw\u00e4hlen.\n\nF\u00fclle folgende Informationen aus: \n- URL: `{webhook_url}`\n- Methode: POST\n- Inhaltstyp: application/json\n\nIn [der Dokumentation] ({docs_url}) findest du Informationen zur Konfiguration der Automation eingehender Daten." }, "step": { "user": { diff --git a/homeassistant/components/mutesync/translations/de.json b/homeassistant/components/mutesync/translations/de.json index 613cac29b1c..dccab9e8d1e 100644 --- a/homeassistant/components/mutesync/translations/de.json +++ b/homeassistant/components/mutesync/translations/de.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_auth": "Aktivieredie Authentifizierung in den Einstellungen von m\u00fctesync > Authentifizierung", + "invalid_auth": "Aktiviere die Authentifizierung in den Einstellungen von m\u00fctesync > Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/risco/translations/de.json b/homeassistant/components/risco/translations/de.json index a5ebcab51b5..77d842353fc 100644 --- a/homeassistant/components/risco/translations/de.json +++ b/homeassistant/components/risco/translations/de.json @@ -44,7 +44,7 @@ "B": "Gruppe B", "C": "Gruppe C", "D": "Gruppe D", - "arm": "Aktiv, abwesend", + "arm": "Aktiv (abwesend)", "partial_arm": "Teilweise aktiv (STAY)" }, "description": "W\u00e4hle aus, welchen Zustand dein Home Assistant-Alarm f\u00fcr jeden von Risco gemeldeten Zustand melden soll", diff --git a/homeassistant/components/sensor/translations/ca.json b/homeassistant/components/sensor/translations/ca.json index f0df998170d..9303635ca60 100644 --- a/homeassistant/components/sensor/translations/ca.json +++ b/homeassistant/components/sensor/translations/ca.json @@ -9,10 +9,18 @@ "is_gas": "Gas actual de {entity_name}", "is_humidity": "Humitat actual de {entity_name}", "is_illuminance": "Il\u00b7luminaci\u00f3 actual de {entity_name}", + "is_nitrogen_dioxide": "Concentraci\u00f3 actual de di\u00f2xid de nitrogen de {entity_name}", + "is_nitrogen_monoxide": "Concentraci\u00f3 actual de mon\u00f2xid de nitrogen de {entity_name}", + "is_nitrous_oxide": "Concentraci\u00f3 actual d'\u00f2xid nitr\u00f3s de {entity_name}", + "is_ozone": "Concentraci\u00f3 actual d'oz\u00f3 de {entity_name}", + "is_pm1": "Concentraci\u00f3 actual de PM1 de {entity_name}", + "is_pm10": "Concentraci\u00f3 actual de PM10 de {entity_name}", + "is_pm25": "Concentraci\u00f3 actual de PM2.5 de {entity_name}", "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_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}", "is_value": "Valor actual de {entity_name}", "is_voltage": "Voltatge actual de {entity_name}" @@ -26,10 +34,18 @@ "gas": "Canvia el gas de {entity_name}", "humidity": "Canvia la humitat de {entity_name}", "illuminance": "Canvia la il\u00b7luminaci\u00f3 de {entity_name}", + "nitrogen_dioxide": "Canvia la concentraci\u00f3 de di\u00f2xid de nitrogen de {entity_name}", + "nitrogen_monoxide": "Canvia la concentraci\u00f3 de mon\u00f2xid de nitrogen de {entity_name}", + "nitrous_oxide": "Canvia la concentraci\u00f3 d'\u00f2xid nitr\u00f3s de {entity_name}", + "ozone": "Canvia la concentraci\u00f3 d'oz\u00f3 de {entity_name}", + "pm1": "Canvia la concentraci\u00f3 de PM1 de {entity_name}", + "pm10": "Canvia la concentraci\u00f3 de PM10 de {entity_name}", + "pm25": "Canvia la concentraci\u00f3 de PM2.5 de {entity_name}", "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}", "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}", "value": "Canvia el valor de {entity_name}", "voltage": "Canvia el voltatge de {entity_name}" diff --git a/homeassistant/components/sensor/translations/de.json b/homeassistant/components/sensor/translations/de.json index c65959b8210..ed6b678480f 100644 --- a/homeassistant/components/sensor/translations/de.json +++ b/homeassistant/components/sensor/translations/de.json @@ -9,10 +9,18 @@ "is_gas": "Aktuelles {entity_name} Gas", "is_humidity": "{entity_name} Feuchtigkeit", "is_illuminance": "Aktuelle {entity_name} Helligkeit", + "is_nitrogen_dioxide": "Aktuelle Stickstoffdioxid-Konzentration von {entity_name}", + "is_nitrogen_monoxide": "Aktuelle Stickstoffmonoxidkonzentration von {entity_name}", + "is_nitrous_oxide": "Aktuelle Lachgaskonzentration von {entity_name}", + "is_ozone": "Aktuelle Ozonkonzentration von {entity_name}", + "is_pm1": "Aktuelle PM1-Konzentrationswert von {entity_name}", + "is_pm10": "Aktuelle PM10-Konzentrationswert von {entity_name}", + "is_pm25": "Aktuelle PM2.5-Konzentration von {entity_name}", "is_power": "Aktuelle {entity_name} Leistung", "is_power_factor": "Aktueller Leistungsfaktor f\u00fcr {entity_name}", "is_pressure": "{entity_name} Druck", "is_signal_strength": "Aktuelle {entity_name} Signalst\u00e4rke", + "is_sulphur_dioxide": "Aktuelle Schwefeldioxid-Konzentration von {entity_name}", "is_temperature": "Aktuelle {entity_name} Temperatur", "is_value": "Aktueller {entity_name} Wert", "is_voltage": "Aktuelle Spannung von {entity_name}" @@ -26,10 +34,18 @@ "gas": "{entity_name} Gas\u00e4nderungen", "humidity": "{entity_name} Feuchtigkeits\u00e4nderungen", "illuminance": "{entity_name} Helligkeits\u00e4nderungen", + "nitrogen_dioxide": "\u00c4nderung der Stickstoffdioxidkonzentration bei {entity_name}", + "nitrogen_monoxide": "\u00c4nderung der Stickstoffmonoxid-Konzentration bei {entity_name}", + "nitrous_oxide": "\u00c4nderung der Lachgaskonzentration bei {entity_name}", + "ozone": "\u00c4nderung der Ozonkonzentration bei {entity_name}", + "pm1": "\u00c4nderung der PM1-Konzentration bei {entity_name}", + "pm10": "\u00c4nderung der PM10-Konzentration bei {entity_name}", + "pm25": "\u00c4nderung der PM2,5-Konzentration bei {entity_name}", "power": "{entity_name} Leistungs\u00e4nderungen", "power_factor": "{entity_name} Leistungsfaktor\u00e4nderung", "pressure": "{entity_name} Druck\u00e4nderungen", "signal_strength": "{entity_name} Signalst\u00e4rke\u00e4nderungen", + "sulphur_dioxide": "\u00c4nderung der Schwefeldioxidkonzentration bei {entity_name}", "temperature": "{entity_name} Temperatur\u00e4nderungen", "value": "{entity_name} Wert\u00e4nderungen", "voltage": "{entity_name} Spannungs\u00e4nderungen" diff --git a/homeassistant/components/sensor/translations/et.json b/homeassistant/components/sensor/translations/et.json index 839f505f6aa..f36391e1e44 100644 --- a/homeassistant/components/sensor/translations/et.json +++ b/homeassistant/components/sensor/translations/et.json @@ -9,10 +9,18 @@ "is_gas": "Praegune {entity_name} gaas", "is_humidity": "Praegune {entity_name} niiskus", "is_illuminance": "Praegune {entity_name} valgustatus", + "is_nitrogen_dioxide": "Praegune {entity_name} l\u00e4mmastikdioksiidi kontsentratsioonitase", + "is_nitrogen_monoxide": "Praegune {entity_name} l\u00e4mmastikmonooksiidi kontsentratsioonitase", + "is_nitrous_oxide": "Praegune {entity_name} dil\u00e4mmastikoksiidi kontsentratsioonitase", + "is_ozone": "Praegune osoonisisalduse tase {entity_name}", + "is_pm1": "Praegune {entity_name} PM1 kontsentratsioonitase", + "is_pm10": "Praegune {entity_name} PM10 kontsentratsioonitase", + "is_pm25": "Praegune {entity_name} PM2.5 kontsentratsioonitase", "is_power": "Praegune {entity_name} toide (v\u00f5imsus)", "is_power_factor": "Praegune {entity_name} v\u00f5imsusfaktor", "is_pressure": "Praegune {entity_name} r\u00f5hk", "is_signal_strength": "Praegune {entity_name} signaali tugevus", + "is_sulphur_dioxide": "Praegune v\u00e4\u00e4veldioksiidi kontsentratsioonitase {entity_name}", "is_temperature": "Praegune {entity_name} temperatuur", "is_value": "Praegune {entity_name} v\u00e4\u00e4rtus", "is_voltage": "Praegune {entity_name}pinge" @@ -26,10 +34,18 @@ "gas": "{entity_name} gaasivahetus", "humidity": "{entity_name} niiskus muutub", "illuminance": "{entity_name} valgustustugevus muutub", + "nitrogen_dioxide": "{entity_name} l\u00e4mmastikdioksiidi kontsentratsiooni muutused", + "nitrogen_monoxide": "{entity_name} l\u00e4mmastikmonooksiidi kontsentratsiooni muutused", + "nitrous_oxide": "{entity_name} l\u00e4mmastikoksiidi kontsentratsiooni muutused", + "ozone": "{entity_name} osooni kontsentratsiooni muutused", + "pm1": "{entity_name} PM1 kontsentratsiooni muutused", + "pm10": "{entity_name} PM10 kontsentratsiooni muutused", + "pm25": "{entity_name} PM2.5 kontsentratsiooni muutused", "power": "{entity_name} energiare\u017eiimi muutub", "power_factor": "{entity_name} v\u00f5imsus muutub", "pressure": "{entity_name} r\u00f5hk muutub", "signal_strength": "{entity_name} signaalitugevus muutub", + "sulphur_dioxide": "{entity_name} v\u00e4\u00e4veldioksiidi kontsentratsiooni muutused", "temperature": "{entity_name} temperatuur muutub", "value": "{entity_name} v\u00e4\u00e4rtus muutub", "voltage": "{entity_name} pingemuutub" diff --git a/homeassistant/components/sensor/translations/hu.json b/homeassistant/components/sensor/translations/hu.json index 1e2aba465cc..58ecdea0f24 100644 --- a/homeassistant/components/sensor/translations/hu.json +++ b/homeassistant/components/sensor/translations/hu.json @@ -9,10 +9,18 @@ "is_gas": "Jelenlegi {entity_name} g\u00e1z", "is_humidity": "{entity_name} aktu\u00e1lis p\u00e1ratartalma", "is_illuminance": "{entity_name} aktu\u00e1lis megvil\u00e1g\u00edt\u00e1sa", + "is_nitrogen_dioxide": "Jelenlegi {entity_name} nitrog\u00e9n-dioxid-koncentr\u00e1ci\u00f3 szint", + "is_nitrogen_monoxide": "Jelenlegi {entity_name} nitrog\u00e9n-monoxid-koncentr\u00e1ci\u00f3 szint", + "is_nitrous_oxide": "Jelenlegi {entity_name} dinitrog\u00e9n-oxid-koncentr\u00e1ci\u00f3 szint", + "is_ozone": "Jelenlegi {entity_name} \u00f3zonkoncentr\u00e1ci\u00f3 szint", + "is_pm1": "Jelenlegi {entity_name} PM1 koncentr\u00e1ci\u00f3 szintje", + "is_pm10": "Jelenlegi {entity_name} PM10 koncentr\u00e1ci\u00f3 szintje", + "is_pm25": "Jelenlegi {entity_name} PM2.5 koncentr\u00e1ci\u00f3 szintje", "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_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", "is_value": "{entity_name} aktu\u00e1lis \u00e9rt\u00e9ke", "is_voltage": "A jelenlegi {entity_name} fesz\u00fclts\u00e9g" @@ -26,10 +34,18 @@ "gas": "{entity_name} g\u00e1z v\u00e1ltoz\u00e1sok", "humidity": "{entity_name} p\u00e1ratartalma v\u00e1ltozik", "illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1sa v\u00e1ltozik", + "nitrogen_dioxide": "{entity_name} nitrog\u00e9n-dioxid koncentr\u00e1ci\u00f3 v\u00e1ltozik", + "nitrogen_monoxide": "{entity_name} nitrog\u00e9n-monoxid koncentr\u00e1ci\u00f3 v\u00e1ltozik", + "nitrous_oxide": "{entity_name} dinitrog\u00e9n-oxid koncentr\u00e1ci\u00f3ja v\u00e1ltozik", + "ozone": "{entity_name} \u00f3zonkoncentr\u00e1ci\u00f3 v\u00e1ltozik", + "pm1": "{entity_name} PM1 koncentr\u00e1ci\u00f3 v\u00e1ltozik", + "pm10": "{entity_name} PM10 koncentr\u00e1ci\u00f3 v\u00e1ltozik", + "pm25": "{entity_name} PM2.5 koncentr\u00e1ci\u00f3 v\u00e1ltozik", "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", "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", "value": "{entity_name} \u00e9rt\u00e9ke v\u00e1ltozik", "voltage": "{entity_name} fesz\u00fclts\u00e9ge v\u00e1ltozik" diff --git a/homeassistant/components/sensor/translations/no.json b/homeassistant/components/sensor/translations/no.json index c9c9542b92b..9af00949510 100644 --- a/homeassistant/components/sensor/translations/no.json +++ b/homeassistant/components/sensor/translations/no.json @@ -9,10 +9,18 @@ "is_gas": "Gjeldende {entity_name} gass", "is_humidity": "Gjeldende {entity_name} fuktighet", "is_illuminance": "Gjeldende {entity_name} belysningsstyrke", + "is_nitrogen_dioxide": "Gjeldende konsentrasjonsniv\u00e5 for {entity_name}", + "is_nitrogen_monoxide": "Gjeldende {entity_name} nitrogenmonoksidkonsentrasjonsniv\u00e5", + "is_nitrous_oxide": "Gjeldende {entity_name} lystgasskonsentrasjonsniv\u00e5", + "is_ozone": "Gjeldende {entity_name} ozonkonsentrasjonsniv\u00e5", + "is_pm1": "Gjeldende {entity_name} PM1 konsentrasjonsniv\u00e5", + "is_pm10": "Gjeldende konsentrasjonsniv\u00e5 for {entity_name}", + "is_pm25": "Gjeldende {entity_name} PM2.5 konsentrasjonsniv\u00e5", "is_power": "Gjeldende {entity_name}-effekt", "is_power_factor": "Gjeldende {entity_name} effektfaktor", "is_pressure": "Gjeldende {entity_name} trykk", "is_signal_strength": "Gjeldende {entity_name} signalstyrke", + "is_sulphur_dioxide": "Gjeldende konsentrasjonsniv\u00e5 for svoveldioksid for {entity_name}", "is_temperature": "Gjeldende {entity_name} temperatur", "is_value": "Gjeldende {entity_name} verdi", "is_voltage": "Gjeldende {entity_name} spenning" @@ -26,10 +34,18 @@ "gas": "{entity_name} gass endres", "humidity": "{entity_name} fuktighets endringer", "illuminance": "{entity_name} belysningsstyrke endringer", + "nitrogen_dioxide": "{entity_name} nitrogendioksidkonsentrasjonsendringer", + "nitrogen_monoxide": "{entity_name} nitrogenmonoksidkonsentrasjonsendringer", + "nitrous_oxide": "{entity_name} endringer i nitrogenoksidskonsentrasjonen", + "ozone": "{entity_name} ozonkonsentrasjonsendringer", + "pm1": "{entity_name} PM1 -konsentrasjon endres", + "pm10": "{entity_name} PM10 -konsentrasjon endres", + "pm25": "{entity_name} PM2.5 konsentrasjon endres", "power": "{entity_name} effektendringer", "power_factor": "{entity_name} effektfaktorendringer", "pressure": "{entity_name} trykk endringer", "signal_strength": "{entity_name} signalstyrkeendringer", + "sulphur_dioxide": "{entity_name} svoveldioksidkonsentrasjon endres", "temperature": "{entity_name} temperaturendringer", "value": "{entity_name} verdi endringer", "voltage": "{entity_name} spenningsendringer" diff --git a/homeassistant/components/sensor/translations/ru.json b/homeassistant/components/sensor/translations/ru.json index 930459c4fc5..641ec453c51 100644 --- a/homeassistant/components/sensor/translations/ru.json +++ b/homeassistant/components/sensor/translations/ru.json @@ -9,10 +9,18 @@ "is_gas": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_humidity": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_illuminance": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_nitrogen_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 \u0430\u0437\u043e\u0442\u0430", + "is_nitrogen_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 \u043c\u043e\u043d\u043e\u043e\u043a\u0441\u0438\u0434\u0430 \u0430\u0437\u043e\u0442\u0430", + "is_nitrous_oxide": "{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 \u0437\u0430\u043a\u0438\u0441\u0438 \u0430\u0437\u043e\u0442\u0430", + "is_ozone": "{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 \u043e\u0437\u043e\u043d\u0430", + "is_pm1": "{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 PM1", + "is_pm10": "{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 PM10", + "is_pm25": "{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 PM2.5", "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_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", "is_value": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "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" @@ -26,10 +34,18 @@ "gas": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435 \u0433\u0430\u0437\u0430", "humidity": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "illuminance": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "nitrogen_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 \u0430\u0437\u043e\u0442\u0430", + "nitrogen_monoxide": "{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 \u043c\u043e\u043d\u043e\u043e\u043a\u0441\u0438\u0434\u0430 \u0430\u0437\u043e\u0442\u0430", + "nitrous_oxide": "{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 \u0437\u0430\u043a\u0438\u0441\u0438 \u0430\u0437\u043e\u0442\u0430", + "ozone": "{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 \u043e\u0437\u043e\u043d\u0430", + "pm1": "{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 PM1", + "pm10": "{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 PM10", + "pm25": "{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 PM2.5", "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", "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", "value": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "voltage": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043d\u0430\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f" diff --git a/homeassistant/components/sensor/translations/zh-Hant.json b/homeassistant/components/sensor/translations/zh-Hant.json index b22ba82f3a4..52ab5878ba3 100644 --- a/homeassistant/components/sensor/translations/zh-Hant.json +++ b/homeassistant/components/sensor/translations/zh-Hant.json @@ -9,10 +9,18 @@ "is_gas": "\u76ee\u524d{entity_name}\u6c23\u9ad4", "is_humidity": "\u76ee\u524d{entity_name}\u6fd5\u5ea6", "is_illuminance": "\u76ee\u524d{entity_name}\u7167\u5ea6", + "is_nitrogen_dioxide": "\u76ee\u524d {entity_name} \u4e8c\u6c27\u5316\u6c2e\u6fc3\u5ea6\u72c0\u614b", + "is_nitrogen_monoxide": "\u76ee\u524d {entity_name} \u4e00\u6c27\u5316\u6c2e\u6fc3\u5ea6\u72c0\u614b", + "is_nitrous_oxide": "\u76ee\u524d {entity_name} \u4e00\u6c27\u5316\u4e8c\u6c2e\u6fc3\u5ea6\u72c0\u614b", + "is_ozone": "\u76ee\u524d {entity_name} \u81ed\u6c27\u6fc3\u5ea6\u72c0\u614b", + "is_pm1": "\u76ee\u524d {entity_name} PM1 \u6fc3\u5ea6\u72c0\u614b", + "is_pm10": "\u76ee\u524d {entity_name} PM10 \u6fc3\u5ea6\u72c0\u614b", + "is_pm25": "\u76ee\u524d {entity_name} PM2.5 \u6fc3\u5ea6\u72c0\u614b", "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_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", "is_value": "\u76ee\u524d{entity_name}\u503c", "is_voltage": "\u76ee\u524d{entity_name}\u96fb\u58d3" @@ -26,10 +34,18 @@ "gas": "{entity_name}\u6c23\u9ad4\u8b8a\u66f4", "humidity": "{entity_name}\u6fd5\u5ea6\u8b8a\u66f4", "illuminance": "{entity_name}\u7167\u5ea6\u8b8a\u66f4", + "nitrogen_dioxide": "{entity_name} \u4e8c\u6c27\u5316\u6c2e\u6fc3\u5ea6\u8b8a\u5316", + "nitrogen_monoxide": "{entity_name} \u4e00\u6c27\u5316\u6c2e\u6fc3\u5ea6\u8b8a\u5316", + "nitrous_oxide": "{entity_name} \u4e00\u6c27\u5316\u4e8c\u6c2e\u6fc3\u5ea6\u8b8a\u5316", + "ozone": "{entity_name} \u81ed\u6c27\u6fc3\u5ea6\u8b8a\u5316", + "pm1": "{entity_name} PM1 \u6fc3\u5ea6\u8b8a\u5316", + "pm10": "{entity_name} PM10 \u6fc3\u5ea6\u8b8a\u5316", + "pm25": "{entity_name} PM2.5 \u6fc3\u5ea6\u8b8a\u5316", "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", "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", "value": "{entity_name}\u503c\u8b8a\u66f4", "voltage": "\u76ee\u524d{entity_name}\u96fb\u58d3\u8b8a\u66f4" diff --git a/homeassistant/components/tractive/translations/ca.json b/homeassistant/components/tractive/translations/ca.json index 4854e13a199..0641dd2737b 100644 --- a/homeassistant/components/tractive/translations/ca.json +++ b/homeassistant/components/tractive/translations/ca.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" + "already_configured": "El dispositiu ja est\u00e0 configurat", + "reauth_failed_existing": "No s'ha pogut actualitzar l'entrada de configuraci\u00f3, elimina la integraci\u00f3 i torna-la a instal\u00b7lar.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", diff --git a/homeassistant/components/tractive/translations/cs.json b/homeassistant/components/tractive/translations/cs.json index de52bfbd7a8..3ad489e1f5e 100644 --- a/homeassistant/components/tractive/translations/cs.json +++ b/homeassistant/components/tractive/translations/cs.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "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/tractive/translations/de.json b/homeassistant/components/tractive/translations/de.json index fbb3411a6c5..cad80fd36a8 100644 --- a/homeassistant/components/tractive/translations/de.json +++ b/homeassistant/components/tractive/translations/de.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_failed_existing": "Der Konfigurationseintrag konnte nicht aktualisiert werden. Bitte entferne die Integration und richte sie erneut ein.", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { diff --git a/homeassistant/components/tractive/translations/et.json b/homeassistant/components/tractive/translations/et.json index 7e9ab892ed4..67adf622ebe 100644 --- a/homeassistant/components/tractive/translations/et.json +++ b/homeassistant/components/tractive/translations/et.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_failed_existing": "Seadekirjet ei \u00f5nnestunud uuendada, eemalda sidumine ja seadista see uuesti.", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "invalid_auth": "Tuvastamine nurjus", diff --git a/homeassistant/components/tractive/translations/no.json b/homeassistant/components/tractive/translations/no.json index 3ae73c02103..a768b453848 100644 --- a/homeassistant/components/tractive/translations/no.json +++ b/homeassistant/components/tractive/translations/no.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "reauth_failed_existing": "Kunne ikke oppdatere konfigurasjonsoppf\u00f8ringen. Fjern integrasjonen og sett den opp igjen.", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning", diff --git a/homeassistant/components/tractive/translations/ru.json b/homeassistant/components/tractive/translations/ru.json index 155e3a99ba5..89042b79b5e 100644 --- a/homeassistant/components/tractive/translations/ru.json +++ b/homeassistant/components/tractive/translations/ru.json @@ -1,7 +1,9 @@ { "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.", + "reauth_failed_existing": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438, \u0443\u0434\u0430\u043b\u0438\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0435\u0451 \u0441\u043d\u043e\u0432\u0430.", + "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.", diff --git a/homeassistant/components/xiaomi_aqara/translations/de.json b/homeassistant/components/xiaomi_aqara/translations/de.json index bc87f461c33..469fa14bcc1 100644 --- a/homeassistant/components/xiaomi_aqara/translations/de.json +++ b/homeassistant/components/xiaomi_aqara/translations/de.json @@ -33,7 +33,7 @@ "data": { "host": "IP-Adresse (optional)", "interface": "Die zu verwendende Netzwerkschnittstelle", - "mac": "MAC-Adresse" + "mac": "MAC-Adresse (optional)" }, "description": "Stelle eine Verbindung zu deinem Xiaomi Aqara Gateway her. Wenn die IP- und MAC-Adressen leer bleiben, wird die automatische Erkennung verwendet", "title": "Xiaomi Aqara Gateway" diff --git a/homeassistant/components/xiaomi_miio/translations/ca.json b/homeassistant/components/xiaomi_miio/translations/ca.json index ff0a24170f6..7e9d7d5c7eb 100644 --- a/homeassistant/components/xiaomi_miio/translations/ca.json +++ b/homeassistant/components/xiaomi_miio/translations/ca.json @@ -10,7 +10,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "cloud_credentials_incomplete": "Credencials del n\u00favol incompletes, introdueix el nom d'usuari, la contrasenya i el pa\u00eds", - "cloud_login_error": "No s'ha pogut iniciar sessi\u00f3 a Xioami Miio Cloud, comprova les credencials.", + "cloud_login_error": "No s'ha pogut iniciar sessi\u00f3 a Xiaomi Miio Cloud, comprova les credencials.", "cloud_no_devices": "No s'han trobat dispositius en aquest compte al n\u00favol de Xiaomi Miio.", "no_device_selected": "No hi ha cap dispositiu seleccionat, selecciona'n un.", "unknown_device": "No es reconeix el model del dispositiu, no es pot configurar el dispositiu mitjan\u00e7ant el flux de configuraci\u00f3." diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json index 17363b347c0..24e639e3a23 100644 --- a/homeassistant/components/xiaomi_miio/translations/de.json +++ b/homeassistant/components/xiaomi_miio/translations/de.json @@ -10,7 +10,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "cloud_credentials_incomplete": "Cloud-Anmeldeinformationen unvollst\u00e4ndig, bitte Benutzernamen, Passwort und Land eingeben", - "cloud_login_error": "Konnte sich nicht bei Xioami Miio Cloud anmelden, \u00fcberpr\u00fcfe die Anmeldedaten.", + "cloud_login_error": "Die Anmeldung bei Xiaomi Miio Cloud ist fehlgeschlagen, \u00fcberpr\u00fcfe die Anmeldedaten.", "cloud_no_devices": "Keine Ger\u00e4te in diesem Xiaomi Miio Cloud-Konto gefunden.", "no_device_selected": "Kein Ger\u00e4t ausgew\u00e4hlt, bitte w\u00e4hle ein Ger\u00e4t aus.", "unknown_device": "Das Ger\u00e4temodell ist nicht bekannt und das Ger\u00e4t kann nicht mithilfe des Assistenten eingerichtet werden." diff --git a/homeassistant/components/xiaomi_miio/translations/et.json b/homeassistant/components/xiaomi_miio/translations/et.json index 92d8ffe048f..4eb326d7f08 100644 --- a/homeassistant/components/xiaomi_miio/translations/et.json +++ b/homeassistant/components/xiaomi_miio/translations/et.json @@ -10,7 +10,7 @@ "error": { "cannot_connect": "\u00dchendus nurjus", "cloud_credentials_incomplete": "Pilve mandaat on poolik, palun t\u00e4ida kasutajanimi, salas\u00f5na ja riik", - "cloud_login_error": "Xioami Miio Cloudi ei saanud sisse logida, kontrolli mandaati.", + "cloud_login_error": "Xiaomi Miio Cloudi ei saanud sisse logida, kontrolli mandaati.", "cloud_no_devices": "Xiaomi Miio pilvekontolt ei leitud \u00fchtegi seadet.", "no_device_selected": "Seadmeid pole valitud, vali \u00fcks seade.", "unknown_device": "Seadme mudel pole teada, seadet ei saa seadistamisvoo abil seadistada." From bd550c4559850b5d819e5a9ada1106cddc2cf943 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 18 Aug 2021 02:40:06 +0200 Subject: [PATCH 291/355] Use AQI, PM1, PM25, PM10 device classes in Airly (#54742) --- homeassistant/components/airly/const.py | 11 ++++++++--- tests/components/airly/test_sensor.py | 12 ++++++++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index e6b87db6f15..79004abbe41 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -6,7 +6,11 @@ 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_HUMIDITY, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, @@ -49,26 +53,27 @@ NO_AIRLY_SENSORS: Final = "There are no Airly sensors in this area yet." SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = ( AirlySensorEntityDescription( key=ATTR_API_CAQI, + device_class=DEVICE_CLASS_AQI, name=ATTR_API_CAQI, native_unit_of_measurement="CAQI", ), AirlySensorEntityDescription( key=ATTR_API_PM1, - icon="mdi:blur", + device_class=DEVICE_CLASS_PM1, name=ATTR_API_PM1, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), AirlySensorEntityDescription( key=ATTR_API_PM25, - icon="mdi:blur", + device_class=DEVICE_CLASS_PM25, name="PM2.5", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), AirlySensorEntityDescription( key=ATTR_API_PM10, - icon="mdi:blur", + device_class=DEVICE_CLASS_PM10, name=ATTR_API_PM10, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, diff --git a/tests/components/airly/test_sensor.py b/tests/components/airly/test_sensor.py index c566702a5b4..cd17c692176 100644 --- a/tests/components/airly/test_sensor.py +++ b/tests/components/airly/test_sensor.py @@ -7,10 +7,13 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, - ATTR_ICON, 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, @@ -38,6 +41,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 entry = registry.async_get("sensor.home_caqi") assert entry @@ -63,7 +67,7 @@ async def test_sensor(hass, aioclient_mock): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM1 assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.home_pm1") @@ -78,7 +82,7 @@ async def test_sensor(hass, aioclient_mock): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM25 assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.home_pm2_5") @@ -93,7 +97,7 @@ async def test_sensor(hass, aioclient_mock): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM10 assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.home_pm10") From 10058ea3f01540cb446fcd067022460ad35dfdac Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 18 Aug 2021 05:35:05 +0200 Subject: [PATCH 292/355] Use new device classes in GIOS integration (#54743) * Use new device classes * Clean up --- homeassistant/components/gios/const.py | 19 ++++++++++++++- homeassistant/components/gios/sensor.py | 1 - tests/components/gios/test_sensor.py | 32 +++++++++++++------------ 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/gios/const.py b/homeassistant/components/gios/const.py index fb96a08ab5b..4f19b0d8a68 100644 --- a/homeassistant/components/gios/const.py +++ b/homeassistant/components/gios/const.py @@ -5,7 +5,16 @@ 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 +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 .model import GiosSensorEntityDescription @@ -36,47 +45,55 @@ SENSOR_TYPES: Final[tuple[GiosSensorEntityDescription, ...]] = ( GiosSensorEntityDescription( key=ATTR_AQI, name="AQI", + device_class=DEVICE_CLASS_AQI, value=None, ), GiosSensorEntityDescription( key=ATTR_C6H6, name="C6H6", + icon="mdi:molecule", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_CO, name="CO", + device_class=DEVICE_CLASS_CO, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_NO2, name="NO2", + device_class=DEVICE_CLASS_NITROGEN_DIOXIDE, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_O3, name="O3", + device_class=DEVICE_CLASS_OZONE, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_PM10, name="PM10", + device_class=DEVICE_CLASS_PM10, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_PM25, name="PM2.5", + device_class=DEVICE_CLASS_PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_SO2, name="SO2", + device_class=DEVICE_CLASS_SULPHUR_DIOXIDE, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=STATE_CLASS_MEASUREMENT, ), diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index c58f08965ec..9ba5e5410b0 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -86,7 +86,6 @@ class GiosSensor(CoordinatorEntity, SensorEntity): "manufacturer": MANUFACTURER, "entry_type": "service", } - self._attr_icon = "mdi:blur" self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{coordinator.gios.station_id}-{description.key}" self._attrs: dict[str, Any] = { diff --git a/tests/components/gios/test_sensor.py b/tests/components/gios/test_sensor.py index 2da3d8e1e8c..adf151f4819 100644 --- a/tests/components/gios/test_sensor.py +++ b/tests/components/gios/test_sensor.py @@ -18,9 +18,17 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( ATTR_ATTRIBUTION, + ATTR_DEVICE_CLASS, 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 @@ -45,7 +53,7 @@ async def test_sensor(hass): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_ICON) == "mdi:molecule" assert state.attributes.get(ATTR_INDEX) == "bardzo dobry" entry = registry.async_get("sensor.home_c6h6") @@ -57,12 +65,12 @@ 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_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) == "dobry" entry = registry.async_get("sensor.home_co") @@ -74,12 +82,12 @@ 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_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) == "dobry" entry = registry.async_get("sensor.home_no2") @@ -91,12 +99,12 @@ 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_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) == "dobry" entry = registry.async_get("sensor.home_o3") @@ -108,12 +116,12 @@ 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_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) == "dobry" entry = registry.async_get("sensor.home_pm10") @@ -125,12 +133,12 @@ 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_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) == "dobry" entry = registry.async_get("sensor.home_pm2_5") @@ -142,12 +150,12 @@ 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_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) == "bardzo dobry" entry = registry.async_get("sensor.home_so2") @@ -159,9 +167,9 @@ 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_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_ICON) == "mdi:blur" entry = registry.async_get("sensor.home_aqi") assert entry @@ -225,7 +233,7 @@ async def test_invalid_indexes(hass): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_ICON) == "mdi:molecule" assert state.attributes.get(ATTR_INDEX) is None entry = registry.async_get("sensor.home_c6h6") @@ -242,7 +250,6 @@ async def test_invalid_indexes(hass): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) is None entry = registry.async_get("sensor.home_co") @@ -259,7 +266,6 @@ async def test_invalid_indexes(hass): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) is None entry = registry.async_get("sensor.home_no2") @@ -276,7 +282,6 @@ async def test_invalid_indexes(hass): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) is None entry = registry.async_get("sensor.home_o3") @@ -293,7 +298,6 @@ async def test_invalid_indexes(hass): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) is None entry = registry.async_get("sensor.home_pm10") @@ -310,7 +314,6 @@ async def test_invalid_indexes(hass): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) is None entry = registry.async_get("sensor.home_pm2_5") @@ -327,7 +330,6 @@ async def test_invalid_indexes(hass): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" assert state.attributes.get(ATTR_INDEX) is None entry = registry.async_get("sensor.home_so2") From d7c1e7c7dcfe1a72bf03f0ee708cbafa3c96b40a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Aug 2021 22:41:01 -0500 Subject: [PATCH 293/355] Adjust yeelight homekit model (#54783) --- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/generated/zeroconf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 7b78f540289..3528b096c67 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -10,6 +10,6 @@ "hostname": "yeelink-*" }], "homekit": { - "models": ["YLDP*"] + "models": ["YLD*"] } } diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 536485f7f55..d973698a34b 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -262,7 +262,7 @@ HOMEKIT = { "Touch HD": "rainmachine", "Welcome": "netatmo", "Wemo": "wemo", - "YLDP*": "yeelight", + "YLD*": "yeelight", "iSmartGate": "gogogate2", "iZone": "izone", "tado": "tado" From 87496ae75c6cb049a1a7d75fdcc518300eb799cc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Aug 2021 22:41:22 -0500 Subject: [PATCH 294/355] Fix HomeKit cover creation with tilt position, open/close, no set position (#54727) --- homeassistant/components/homekit/accessories.py | 7 ++++++- tests/components/homekit/test_get_accessories.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index c3fac44486c..ec6ef670f44 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -134,10 +134,15 @@ def get_accessory(hass, driver, state, aid, config): # noqa: C901 and features & cover.SUPPORT_SET_POSITION ): a_type = "Window" - elif features & (cover.SUPPORT_SET_POSITION | cover.SUPPORT_SET_TILT_POSITION): + elif features & cover.SUPPORT_SET_POSITION: a_type = "WindowCovering" elif features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE): a_type = "WindowCoveringBasic" + elif features & cover.SUPPORT_SET_TILT_POSITION: + # WindowCovering and WindowCoveringBasic both support tilt + # only WindowCovering can handle the covers that are missing + # SUPPORT_SET_POSITION, SUPPORT_OPEN, and SUPPORT_CLOSE + a_type = "WindowCovering" elif state.domain == "fan": a_type = "Fan" diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 1b220153195..af98f6a45f9 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -149,6 +149,18 @@ def test_types(type_name, entity_id, state, attrs, config): "open", {ATTR_SUPPORTED_FEATURES: (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE)}, ), + ( + "WindowCoveringBasic", + "cover.open_window", + "open", + { + ATTR_SUPPORTED_FEATURES: ( + cover.SUPPORT_OPEN + | cover.SUPPORT_CLOSE + | cover.SUPPORT_SET_TILT_POSITION + ) + }, + ), ], ) def test_type_covers(type_name, entity_id, state, attrs): From c65f769130753b42c3551bcdc441c2cebfa80398 Mon Sep 17 00:00:00 2001 From: Christopher Kochan <5183896+crkochan@users.noreply.github.com> Date: Wed, 18 Aug 2021 01:29:02 -0500 Subject: [PATCH 295/355] Update sense_energy to version 0.9.2 (#54787) --- homeassistant/components/emulated_kasa/manifest.json | 2 +- homeassistant/components/sense/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json index 419a34db98c..bb3ac2082f8 100644 --- a/homeassistant/components/emulated_kasa/manifest.json +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -2,7 +2,7 @@ "domain": "emulated_kasa", "name": "Emulated Kasa", "documentation": "https://www.home-assistant.io/integrations/emulated_kasa", - "requirements": ["sense_energy==0.9.0"], + "requirements": ["sense_energy==0.9.2"], "codeowners": ["@kbickar"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 0bde2f7a7a7..16cecd1cd97 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -2,7 +2,7 @@ "domain": "sense", "name": "Sense", "documentation": "https://www.home-assistant.io/integrations/sense", - "requirements": ["sense_energy==0.9.0"], + "requirements": ["sense_energy==0.9.2"], "codeowners": ["@kbickar"], "config_flow": true, "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index efb249aac12..0c2674bcb9a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2100,7 +2100,7 @@ sense-hat==2.2.0 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.9.0 +sense_energy==0.9.2 # homeassistant.components.sentry sentry-sdk==1.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7cf5441182e..4575c0c82ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1163,7 +1163,7 @@ screenlogicpy==0.4.1 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.9.0 +sense_energy==0.9.2 # homeassistant.components.sentry sentry-sdk==1.3.0 From 85d9890447968241b00c3d44277f175d3538234d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Aug 2021 08:59:13 +0200 Subject: [PATCH 296/355] Bump dessant/lock-threads from 2.1.1 to 2.1.2 (#54791) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 62c7299c2b8..96fc69e3b68 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -9,7 +9,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v2.1.1 + - uses: dessant/lock-threads@v2.1.2 with: github-token: ${{ github.token }} issue-lock-inactive-days: "30" From e1926caeb91345441ba133ef7768eb887d2e060d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 10:03:27 +0200 Subject: [PATCH 297/355] Remove STATE_CLASS_TOTAL and last_reset from sensor (#54755) * Remove STATE_CLASS_TOTAL * Update mill sensor * Update tests * Kill last_reset * Return ATTR_LAST_RESET to utility_meter * Update energy cost sensor * Restore last_reset for backwards compatibility * Re-add and update deprecation warning * Update tests * Fix utility_meter * Update EnergyCostSensor * Tweak * Fix rebase mistake * Fix test --- homeassistant/components/energy/sensor.py | 37 +++-- homeassistant/components/mill/sensor.py | 26 +--- homeassistant/components/recorder/models.py | 2 - .../components/recorder/statistics.py | 2 - homeassistant/components/sensor/__init__.py | 18 +-- homeassistant/components/sensor/recorder.py | 41 ++---- .../components/utility_meter/sensor.py | 13 +- tests/components/energy/test_sensor.py | 41 +++--- tests/components/history/test_init.py | 1 - tests/components/recorder/test_statistics.py | 3 - tests/components/sensor/test_init.py | 9 +- tests/components/sensor/test_recorder.py | 126 ++---------------- 12 files changed, 68 insertions(+), 251 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index fd36611acaf..497c762add9 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -6,9 +6,8 @@ import logging from typing import Any, Final, Literal, TypeVar, cast from homeassistant.components.sensor import ( - ATTR_LAST_RESET, DEVICE_CLASS_MONETARY, - STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.const import ( @@ -17,11 +16,10 @@ from homeassistant.const import ( ENERGY_WATT_HOUR, VOLUME_CUBIC_METERS, ) -from homeassistant.core import HomeAssistant, State, callback, split_entity_id +from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -import homeassistant.util.dt as dt_util +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from .const import DOMAIN from .data import EnergyManager, async_get_manager @@ -203,16 +201,15 @@ class EnergyCostSensor(SensorEntity): f"{config[adapter.entity_energy_key]}_{adapter.entity_id_suffix}" ) self._attr_device_class = DEVICE_CLASS_MONETARY - self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_state_class = STATE_CLASS_TOTAL_INCREASING self._config = config - self._last_energy_sensor_state: State | None = None + self._last_energy_sensor_state: StateType | None = None self._cur_value = 0.0 - def _reset(self, energy_state: State) -> None: + def _reset(self, energy_state: StateType) -> None: """Reset the cost sensor.""" self._attr_native_value = 0.0 self._cur_value = 0.0 - self._attr_last_reset = dt_util.utcnow() self._last_energy_sensor_state = energy_state self.async_write_ha_state() @@ -223,7 +220,7 @@ class EnergyCostSensor(SensorEntity): cast(str, self._config[self._adapter.entity_energy_key]) ) - if energy_state is None or ATTR_LAST_RESET not in energy_state.attributes: + if energy_state is None: return try: @@ -259,7 +256,7 @@ class EnergyCostSensor(SensorEntity): if self._last_energy_sensor_state is None: # Initialize as it's the first time all required entities are in place. - self._reset(energy_state) + self._reset(energy_state.state) return energy_unit = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -280,19 +277,15 @@ class EnergyCostSensor(SensorEntity): ) return - if ( - energy_state.attributes[ATTR_LAST_RESET] - != self._last_energy_sensor_state.attributes[ATTR_LAST_RESET] - ): + if energy < float(self._last_energy_sensor_state): # Energy meter was reset, reset cost sensor too - self._reset(energy_state) - else: - # Update with newly incurred cost - old_energy_value = float(self._last_energy_sensor_state.state) - self._cur_value += (energy - old_energy_value) * energy_price - self._attr_native_value = round(self._cur_value, 2) + self._reset(0) + # Update with newly incurred cost + old_energy_value = float(self._last_energy_sensor_state) + self._cur_value += (energy - old_energy_value) * energy_price + self._attr_native_value = round(self._cur_value, 2) - self._last_energy_sensor_state = energy_state + self._last_energy_sensor_state = energy_state.state async def async_added_to_hass(self) -> None: """Register callbacks.""" diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index 5241f95abdb..ce7704ad1be 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -2,11 +2,10 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.const import ENERGY_KILO_WATT_HOUR -from homeassistant.util import dt as dt_util from .const import CONSUMPTION_TODAY, CONSUMPTION_YEAR, DOMAIN, MANUFACTURER @@ -29,7 +28,7 @@ class MillHeaterEnergySensor(SensorEntity): _attr_device_class = DEVICE_CLASS_ENERGY _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR - _attr_state_class = STATE_CLASS_TOTAL + _attr_state_class = STATE_CLASS_TOTAL_INCREASING def __init__(self, heater, mill_data_connection, sensor_type): """Initialize the sensor.""" @@ -45,16 +44,6 @@ class MillHeaterEnergySensor(SensorEntity): "manufacturer": MANUFACTURER, "model": f"generation {1 if heater.is_gen1 else 2}", } - if self._sensor_type == CONSUMPTION_TODAY: - self._attr_last_reset = dt_util.as_utc( - dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) - ) - elif self._sensor_type == CONSUMPTION_YEAR: - self._attr_last_reset = dt_util.as_utc( - dt_util.now().replace( - month=1, day=1, hour=0, minute=0, second=0, microsecond=0 - ) - ) async def async_update(self): """Retrieve latest state.""" @@ -71,15 +60,4 @@ class MillHeaterEnergySensor(SensorEntity): self._attr_native_value = _state return - if self.state is not None and _state < self.state: - if self._sensor_type == CONSUMPTION_TODAY: - self._attr_last_reset = dt_util.as_utc( - dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) - ) - elif self._sensor_type == CONSUMPTION_YEAR: - self._attr_last_reset = dt_util.as_utc( - dt_util.now().replace( - month=1, day=1, hour=0, minute=0, second=0, microsecond=0 - ) - ) self._attr_native_value = _state diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index ff64deb60cd..fe75ba1cb50 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -218,7 +218,6 @@ class StatisticData(TypedDict, total=False): mean: float min: float max: float - last_reset: datetime | None state: float sum: float @@ -242,7 +241,6 @@ class Statistics(Base): # type: ignore mean = Column(Float()) min = Column(Float()) max = Column(Float()) - last_reset = Column(DATETIME_TYPE) state = Column(Float()) sum = Column(Float()) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index b91e4d160df..6017f050419 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -43,7 +43,6 @@ QUERY_STATISTICS = [ Statistics.mean, Statistics.min, Statistics.max, - Statistics.last_reset, Statistics.state, Statistics.sum, ] @@ -375,7 +374,6 @@ def _sorted_statistics_to_dict( "mean": convert(db_state.mean, units), "min": convert(db_state.min, units), "max": convert(db_state.max, units), - "last_reset": _process_timestamp_to_utc_isoformat(db_state.last_reset), "state": convert(db_state.state, units), "sum": convert(db_state.sum, units), } diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 950af5a1375..94fb08c66b1 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -51,7 +51,7 @@ from homeassistant.helpers.typing import ConfigType, StateType _LOGGER: Final = logging.getLogger(__name__) -ATTR_LAST_RESET: Final = "last_reset" +ATTR_LAST_RESET: Final = "last_reset" # Deprecated, to be removed in 2021.11 ATTR_STATE_CLASS: Final = "state_class" DOMAIN: Final = "sensor" @@ -91,14 +91,11 @@ DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) # The state represents a measurement in present time STATE_CLASS_MEASUREMENT: Final = "measurement" -# The state represents a total amount, e.g. a value of a stock portfolio -STATE_CLASS_TOTAL: Final = "total" # The state represents a monotonically increasing total, e.g. an amount of consumed gas STATE_CLASS_TOTAL_INCREASING: Final = "total_increasing" STATE_CLASSES: Final[list[str]] = [ STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL, STATE_CLASS_TOTAL_INCREASING, ] @@ -132,7 +129,7 @@ class SensorEntityDescription(EntityDescription): """A class that describes sensor entities.""" state_class: str | None = None - last_reset: datetime | None = None + last_reset: datetime | None = None # Deprecated, to be removed in 2021.11 native_unit_of_measurement: str | None = None @@ -140,7 +137,7 @@ class SensorEntity(Entity): """Base class for sensor entities.""" entity_description: SensorEntityDescription - _attr_last_reset: datetime | None + _attr_last_reset: datetime | None # Deprecated, to be removed in 2021.11 _attr_native_unit_of_measurement: str | None _attr_native_value: StateType = None _attr_state_class: str | None @@ -157,7 +154,7 @@ class SensorEntity(Entity): return None @property - def last_reset(self) -> datetime | None: + def last_reset(self) -> datetime | None: # Deprecated, to be removed in 2021.11 """Return the time when the sensor was last reset, if any.""" if hasattr(self, "_attr_last_reset"): return self._attr_last_reset @@ -187,10 +184,9 @@ class SensorEntity(Entity): report_issue = self._suggest_report_issue() _LOGGER.warning( "Entity %s (%s) with state_class %s has set last_reset. Setting " - "last_reset for entities with state_class other than 'total' is " - "deprecated and will be removed from Home Assistant Core 2021.10. " - "Please update your configuration if state_class is manually " - "configured, otherwise %s", + "last_reset is deprecated and will be unsupported from Home " + "Assistant Core 2021.11. Please update your configuration if " + "state_class is manually configured, otherwise %s", self.entity_id, type(self), self.state_class, diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 48f80bab5c2..2cbca09c09d 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -17,7 +17,6 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL, STATE_CLASS_TOTAL_INCREASING, STATE_CLASSES, ) @@ -43,7 +42,6 @@ from homeassistant.const import ( VOLUME_CUBIC_METERS, ) from homeassistant.core import HomeAssistant, State -import homeassistant.util.dt as dt_util import homeassistant.util.pressure as pressure_util import homeassistant.util.temperature as temperature_util import homeassistant.util.volume as volume_util @@ -53,11 +51,6 @@ from . import ATTR_LAST_RESET, DOMAIN _LOGGER = logging.getLogger(__name__) DEVICE_CLASS_OR_UNIT_STATISTICS = { - STATE_CLASS_TOTAL: { - DEVICE_CLASS_ENERGY: {"sum"}, - DEVICE_CLASS_GAS: {"sum"}, - DEVICE_CLASS_MONETARY: {"sum"}, - }, STATE_CLASS_MEASUREMENT: { DEVICE_CLASS_BATTERY: {"mean", "min", "max"}, DEVICE_CLASS_HUMIDITY: {"mean", "min", "max"}, @@ -65,7 +58,7 @@ DEVICE_CLASS_OR_UNIT_STATISTICS = { DEVICE_CLASS_PRESSURE: {"mean", "min", "max"}, DEVICE_CLASS_TEMPERATURE: {"mean", "min", "max"}, PERCENTAGE: {"mean", "min", "max"}, - # Deprecated, support will be removed in Home Assistant 2021.10 + # Deprecated, support will be removed in Home Assistant 2021.11 DEVICE_CLASS_ENERGY: {"sum"}, DEVICE_CLASS_GAS: {"sum"}, DEVICE_CLASS_MONETARY: {"sum"}, @@ -73,6 +66,7 @@ DEVICE_CLASS_OR_UNIT_STATISTICS = { STATE_CLASS_TOTAL_INCREASING: { DEVICE_CLASS_ENERGY: {"sum"}, DEVICE_CLASS_GAS: {"sum"}, + DEVICE_CLASS_MONETARY: {"sum"}, }, } @@ -279,13 +273,11 @@ def compile_statistics( stat["mean"] = _time_weighted_average(fstates, start, end) if "sum" in wanted_statistics: - last_reset = old_last_reset = None new_state = old_state = None _sum = 0 last_stats = statistics.get_last_statistics(hass, 1, entity_id) 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"] new_state = old_state = last_stats[entity_id][0]["state"] _sum = last_stats[entity_id][0]["sum"] @@ -299,13 +291,7 @@ def compile_statistics( continue reset = False - if ( - state_class != STATE_CLASS_TOTAL_INCREASING - and (last_reset := state.attributes.get("last_reset")) - != old_last_reset - ): - reset = True - elif old_state is None and last_reset is None: + if old_state is None: reset = True elif state_class == STATE_CLASS_TOTAL_INCREASING and ( old_state is None or (new_state is not None and fstate < new_state) @@ -318,21 +304,14 @@ def compile_statistics( _sum += new_state - old_state # ..and update the starting point new_state = fstate - old_last_reset = last_reset - # Force a new cycle for STATE_CLASS_TOTAL_INCREASING to start at 0 - if state_class == STATE_CLASS_TOTAL_INCREASING and old_state: - old_state = 0 + # Force a new cycle to start at 0 + if old_state is not None: + old_state = 0.0 else: old_state = new_state else: new_state = fstate - # Deprecated, will be removed in Home Assistant 2021.10 - if last_reset is None and state_class == STATE_CLASS_MEASUREMENT: - # No valid updates - result.pop(entity_id) - continue - if new_state is None or old_state is None: # No valid updates result.pop(entity_id) @@ -340,8 +319,6 @@ def compile_statistics( # Update the sum with the last state _sum += new_state - old_state - if last_reset is not None: - stat["last_reset"] = dt_util.parse_datetime(last_reset) stat["sum"] = _sum stat["state"] = new_state @@ -365,7 +342,11 @@ def list_statistic_ids(hass: HomeAssistant, statistic_type: str | None = None) - state = hass.states.get(entity_id) assert state - if "sum" in provided_statistics and ATTR_LAST_RESET not in state.attributes: + if ( + "sum" in provided_statistics + and ATTR_LAST_RESET not in state.attributes + and state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + ): continue native_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 1ff201aaceb..84533efdcf5 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -5,11 +5,7 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import ( - ATTR_LAST_RESET, - STATE_CLASS_MEASUREMENT, - SensorEntity, -) +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, @@ -58,6 +54,7 @@ ATTR_SOURCE_ID = "source" ATTR_STATUS = "status" ATTR_PERIOD = "meter_period" ATTR_LAST_PERIOD = "last_period" +ATTR_LAST_RESET = "last_reset" ATTR_TARIFF = "tariff" DEVICE_CLASS_MAP = { @@ -352,6 +349,7 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): ATTR_SOURCE_ID: self._sensor_source_id, ATTR_STATUS: PAUSED if self._collecting is None else COLLECTING, ATTR_LAST_PERIOD: self._last_period, + ATTR_LAST_RESET: self._last_reset.isoformat(), } if self._period is not None: state_attr[ATTR_PERIOD] = self._period @@ -363,8 +361,3 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): def icon(self): """Return the icon to use in the frontend, if any.""" return ICON - - @property - def last_reset(self): - """Return the time when the sensor was last reset.""" - return self._last_reset diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index 1e89c05fbd6..ea183ec52f4 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -7,9 +7,8 @@ import pytest from homeassistant.components.energy import data from homeassistant.components.sensor import ( - ATTR_LAST_RESET, ATTR_STATE_CLASS, - STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, ) from homeassistant.components.sensor.recorder import compile_statistics from homeassistant.const import ( @@ -131,14 +130,13 @@ async def test_cost_sensor_price_entity( } now = dt_util.utcnow() - last_reset = dt_util.utc_from_timestamp(0).isoformat() # Optionally initialize dependent entities if initial_energy is not None: hass.states.async_set( usage_sensor_entity_id, initial_energy, - {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, ) hass.states.async_set("sensor.energy_price", "1") @@ -148,9 +146,7 @@ async def test_cost_sensor_price_entity( state = hass.states.get(cost_sensor_entity_id) assert state.state == initial_cost assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY - if initial_cost != "unknown": - assert state.attributes[ATTR_LAST_RESET] == now.isoformat() - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR" # Optional late setup of dependent entities @@ -160,7 +156,6 @@ async def test_cost_sensor_price_entity( usage_sensor_entity_id, "0", { - "last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, ) @@ -169,8 +164,7 @@ async def test_cost_sensor_price_entity( 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_LAST_RESET] == now.isoformat() - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR" # # Unique ID temp disabled @@ -182,7 +176,7 @@ async def test_cost_sensor_price_entity( hass.states.async_set( usage_sensor_entity_id, "10", - {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, ) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) @@ -206,7 +200,7 @@ async def test_cost_sensor_price_entity( hass.states.async_set( usage_sensor_entity_id, "14.5", - {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, ) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) @@ -218,32 +212,31 @@ async def test_cost_sensor_price_entity( assert cost_sensor_entity_id in statistics assert statistics[cost_sensor_entity_id]["stat"]["sum"] == 19.0 - # Energy sensor is reset, with start point at 4kWh - last_reset = (now + timedelta(seconds=1)).isoformat() + # Energy sensor is reset, with initial state at 4kWh, 0 kWh is used as zero-point hass.states.async_set( usage_sensor_entity_id, "4", - {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, ) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) - assert state.state == "0.0" # 0 EUR + (4-4) kWh * 2 EUR/kWh = 0 EUR + assert state.state == "8.0" # 0 EUR + (4-0) kWh * 2 EUR/kWh = 8 EUR # Energy use bumped to 10 kWh hass.states.async_set( usage_sensor_entity_id, "10", - {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, ) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) - assert state.state == "12.0" # 0 EUR + (10-4) kWh * 2 EUR/kWh = 12 EUR + assert state.state == "20.0" # 8 EUR + (10-4) kWh * 2 EUR/kWh = 20 EUR # Check generated statistics await async_wait_recording_done_without_instance(hass) statistics = await hass.loop.run_in_executor(None, _compile_statistics, hass) assert cost_sensor_entity_id in statistics - assert statistics[cost_sensor_entity_id]["stat"]["sum"] == 31.0 + assert statistics[cost_sensor_entity_id]["stat"]["sum"] == 39.0 async def test_cost_sensor_handle_wh(hass, hass_storage) -> None: @@ -272,12 +265,11 @@ async def test_cost_sensor_handle_wh(hass, hass_storage) -> None: } now = dt_util.utcnow() - last_reset = dt_util.utc_from_timestamp(0).isoformat() hass.states.async_set( "sensor.energy_consumption", 10000, - {"last_reset": last_reset, "unit_of_measurement": ENERGY_WATT_HOUR}, + {"unit_of_measurement": ENERGY_WATT_HOUR}, ) with patch("homeassistant.util.dt.utcnow", return_value=now): @@ -290,7 +282,7 @@ async def test_cost_sensor_handle_wh(hass, hass_storage) -> None: hass.states.async_set( "sensor.energy_consumption", 20000, - {"last_reset": last_reset, "unit_of_measurement": ENERGY_WATT_HOUR}, + {"unit_of_measurement": ENERGY_WATT_HOUR}, ) await hass.async_block_till_done() @@ -318,12 +310,11 @@ async def test_cost_sensor_handle_gas(hass, hass_storage) -> None: } now = dt_util.utcnow() - last_reset = dt_util.utc_from_timestamp(0).isoformat() hass.states.async_set( "sensor.gas_consumption", 100, - {"last_reset": last_reset, "unit_of_measurement": VOLUME_CUBIC_METERS}, + {"unit_of_measurement": VOLUME_CUBIC_METERS}, ) with patch("homeassistant.util.dt.utcnow", return_value=now): @@ -336,7 +327,7 @@ async def test_cost_sensor_handle_gas(hass, hass_storage) -> None: hass.states.async_set( "sensor.gas_consumption", 200, - {"last_reset": last_reset, "unit_of_measurement": VOLUME_CUBIC_METERS}, + {"unit_of_measurement": VOLUME_CUBIC_METERS}, ) await hass.async_block_till_done() diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 7909d8f0239..8de44843626 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -911,7 +911,6 @@ async def test_statistics_during_period( "mean": approx(value), "min": approx(value), "max": approx(value), - "last_reset": None, "state": None, "sum": None, } diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 0468cc26a23..83995b0c0ac 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -44,7 +44,6 @@ def test_compile_hourly_statistics(hass_recorder): "mean": approx(14.915254237288135), "min": approx(10.0), "max": approx(20.0), - "last_reset": None, "state": None, "sum": None, } @@ -54,7 +53,6 @@ def test_compile_hourly_statistics(hass_recorder): "mean": approx(20.0), "min": approx(20.0), "max": approx(20.0), - "last_reset": None, "state": None, "sum": None, } @@ -127,7 +125,6 @@ def test_rename_entity(hass_recorder): "mean": approx(14.915254237288135), "min": approx(10.0), "max": approx(20.0), - "last_reset": None, "state": None, "sum": None, } diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 793bcaf4f99..5ff2cad9edc 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -44,9 +44,8 @@ async def test_deprecated_last_reset(hass, caplog, enable_custom_integrations): assert ( "Entity sensor.test () " - "with state_class measurement has set last_reset. Setting last_reset for " - "entities with state_class other than 'total' is deprecated and will be " - "removed from Home Assistant Core 2021.10. Please update your configuration if " - "state_class is manually configured, otherwise report it to the custom " - "component author." + "with state_class measurement has set last_reset. Setting last_reset is " + "deprecated and will be unsupported from Home Assistant Core 2021.11. Please " + "update your configuration if state_class is manually configured, otherwise " + "report it to the custom component author." ) in caplog.text diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index d4dee872823..3a2572f8141 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -95,7 +95,6 @@ def test_compile_hourly_statistics( "mean": approx(mean), "min": approx(min), "max": approx(max), - "last_reset": None, "state": None, "sum": None, } @@ -145,7 +144,6 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes "mean": approx(16.440677966101696), "min": approx(10.0), "max": approx(30.0), - "last_reset": None, "state": None, "sum": None, } @@ -154,7 +152,6 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes assert "Error while processing event StatisticsTask" not in caplog.text -@pytest.mark.parametrize("state_class", ["measurement", "total"]) @pytest.mark.parametrize( "device_class,unit,native_unit,factor", [ @@ -167,7 +164,7 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes ], ) def test_compile_hourly_sum_statistics_amount( - hass_recorder, caplog, state_class, device_class, unit, native_unit, factor + hass_recorder, caplog, device_class, unit, native_unit, factor ): """Test compiling hourly statistics.""" zero = dt_util.utcnow() @@ -176,7 +173,7 @@ def test_compile_hourly_sum_statistics_amount( setup_component(hass, "sensor", {}) attributes = { "device_class": device_class, - "state_class": state_class, + "state_class": "measurement", "unit_of_measurement": unit, "last_reset": None, } @@ -209,7 +206,6 @@ def test_compile_hourly_sum_statistics_amount( "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(zero), "state": approx(factor * seq[2]), "sum": approx(factor * 10.0), }, @@ -219,89 +215,6 @@ def test_compile_hourly_sum_statistics_amount( "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), - "state": approx(factor * seq[5]), - "sum": approx(factor * 10.0), - }, - { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), - "max": None, - "mean": None, - "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), - "state": approx(factor * seq[8]), - "sum": approx(factor * 40.0), - }, - ] - } - assert "Error while processing event StatisticsTask" not in caplog.text - - -@pytest.mark.parametrize( - "device_class,unit,native_unit,factor", - [ - ("energy", "kWh", "kWh", 1), - ("energy", "Wh", "kWh", 1 / 1000), - ("monetary", "EUR", "EUR", 1), - ("monetary", "SEK", "SEK", 1), - ("gas", "m³", "m³", 1), - ("gas", "ft³", "m³", 0.0283168466), - ], -) -def test_compile_hourly_sum_statistics_total_no_reset( - hass_recorder, caplog, device_class, unit, native_unit, factor -): - """Test compiling hourly statistics.""" - zero = dt_util.utcnow() - hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] - setup_component(hass, "sensor", {}) - attributes = { - "device_class": device_class, - "state_class": "total", - "unit_of_measurement": unit, - } - seq = [10, 15, 20, 10, 30, 40, 50, 60, 70] - - four, eight, states = record_meter_states( - hass, zero, "sensor.test1", attributes, seq - ) - hist = history.get_significant_states( - hass, zero - timedelta.resolution, eight + timedelta.resolution - ) - assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"] - - recorder.do_adhoc_statistics(period="hourly", start=zero) - wait_recording_done(hass) - recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=1)) - wait_recording_done(hass) - recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=2)) - wait_recording_done(hass) - statistic_ids = list_statistic_ids(hass) - assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} - ] - stats = statistics_during_period(hass, zero) - assert stats == { - "sensor.test1": [ - { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "max": None, - "mean": None, - "min": None, - "last_reset": None, - "state": approx(factor * seq[2]), - "sum": approx(factor * 10.0), - }, - { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), - "max": None, - "mean": None, - "min": None, - "last_reset": None, "state": approx(factor * seq[5]), "sum": approx(factor * 30.0), }, @@ -311,7 +224,6 @@ def test_compile_hourly_sum_statistics_total_no_reset( "max": None, "mean": None, "min": None, - "last_reset": None, "state": approx(factor * seq[8]), "sum": approx(factor * 60.0), }, @@ -371,7 +283,6 @@ def test_compile_hourly_sum_statistics_total_increasing( "max": None, "mean": None, "min": None, - "last_reset": None, "state": approx(factor * seq[2]), "sum": approx(factor * 10.0), }, @@ -381,7 +292,6 @@ def test_compile_hourly_sum_statistics_total_increasing( "max": None, "mean": None, "min": None, - "last_reset": None, "state": approx(factor * seq[5]), "sum": approx(factor * 50.0), }, @@ -391,7 +301,6 @@ def test_compile_hourly_sum_statistics_total_increasing( "max": None, "mean": None, "min": None, - "last_reset": None, "state": approx(factor * seq[8]), "sum": approx(factor * 80.0), }, @@ -458,7 +367,6 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(zero), "state": approx(20.0), "sum": approx(10.0), }, @@ -468,9 +376,8 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(40.0), - "sum": approx(10.0), + "sum": approx(30.0), }, { "statistic_id": "sensor.test1", @@ -478,9 +385,8 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(70.0), - "sum": approx(40.0), + "sum": approx(60.0), }, ] } @@ -541,7 +447,6 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(zero), "state": approx(20.0), "sum": approx(10.0), }, @@ -551,9 +456,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(40.0), - "sum": approx(10.0), + "sum": approx(30.0), }, { "statistic_id": "sensor.test1", @@ -561,9 +465,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(70.0), - "sum": approx(40.0), + "sum": approx(60.0), }, ], "sensor.test2": [ @@ -573,7 +476,6 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(zero), "state": approx(130.0), "sum": approx(20.0), }, @@ -583,9 +485,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(45.0), - "sum": approx(-95.0), + "sum": approx(-65.0), }, { "statistic_id": "sensor.test2", @@ -593,9 +494,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(75.0), - "sum": approx(-65.0), + "sum": approx(-35.0), }, ], "sensor.test3": [ @@ -605,7 +505,6 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(zero), "state": approx(5.0 / 1000), "sum": approx(5.0 / 1000), }, @@ -615,9 +514,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(50.0 / 1000), - "sum": approx(30.0 / 1000), + "sum": approx(50.0 / 1000), }, { "statistic_id": "sensor.test3", @@ -625,9 +523,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(90.0 / 1000), - "sum": approx(70.0 / 1000), + "sum": approx(90.0 / 1000), }, ], } @@ -678,7 +575,6 @@ def test_compile_hourly_statistics_unchanged( "mean": approx(value), "min": approx(value), "max": approx(value), - "last_reset": None, "state": None, "sum": None, } @@ -710,7 +606,6 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder, caplog): "mean": approx(21.1864406779661), "min": approx(10.0), "max": approx(25.0), - "last_reset": None, "state": None, "sum": None, } @@ -767,7 +662,6 @@ def test_compile_hourly_statistics_unavailable( "mean": approx(value), "min": approx(value), "max": approx(value), - "last_reset": None, "state": None, "sum": None, } From 73d03bdf1d34a40cca52372f109769243ec40190 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 18 Aug 2021 10:14:03 +0200 Subject: [PATCH 298/355] Add Gas device class to DSMR Reader (#54748) --- homeassistant/components/dsmr_reader/definitions.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index 6edf2972aa4..533b2f0dd38 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CURRENCY_EURO, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_POWER, DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLTAGE, @@ -201,14 +202,14 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( DSMRReaderSensorEntityDescription( key="dsmr/consumption/gas/delivered", name="Gas usage", - icon="mdi:fire", + device_class=DEVICE_CLASS_GAS, native_unit_of_measurement=VOLUME_CUBIC_METERS, state_class=STATE_CLASS_TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/consumption/gas/currently_delivered", name="Current gas usage", - icon="mdi:fire", + device_class=DEVICE_CLASS_GAS, native_unit_of_measurement=VOLUME_CUBIC_METERS, state_class=STATE_CLASS_MEASUREMENT, ), From 102af02d8a235d27b3eef9e9893b23e480e6c4c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 18 Aug 2021 11:21:39 +0200 Subject: [PATCH 299/355] Tibber data coordinator (#53619) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Tibber data coordinator Signed-off-by: Daniel Hjelseth Høyer * Fix comments Signed-off-by: Daniel Hjelseth Høyer * Fix comments Signed-off-by: Daniel Hjelseth Høyer * Fix comments Signed-off-by: Daniel Hjelseth Høyer * Remove whitespace Co-authored-by: Martin Hjelmare --- homeassistant/components/tibber/sensor.py | 199 +++++++++++----------- 1 file changed, 99 insertions(+), 100 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 15bf2a2017e..080da3fca13 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -26,17 +26,15 @@ from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, + EVENT_HOMEASSISTANT_STOP, PERCENTAGE, POWER_WATT, SIGNAL_STRENGTH_DECIBELS, ) 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.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from homeassistant.helpers.entity_registry import async_get as async_get_entity_reg from homeassistant.util import Throttle, dt as dt_util @@ -66,40 +64,40 @@ class TibberSensorEntityDescription(SensorEntityDescription): reset_type: ResetType | None = None -RT_SENSOR_MAP: dict[str, TibberSensorEntityDescription] = { - "averagePower": TibberSensorEntityDescription( +RT_SENSORS: tuple[TibberSensorEntityDescription, ...] = ( + TibberSensorEntityDescription( key="averagePower", name="average power", device_class=DEVICE_CLASS_POWER, native_unit_of_measurement=POWER_WATT, ), - "power": TibberSensorEntityDescription( + TibberSensorEntityDescription( key="power", name="power", device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), - "powerProduction": TibberSensorEntityDescription( + TibberSensorEntityDescription( key="powerProduction", name="power production", device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), - "minPower": TibberSensorEntityDescription( + TibberSensorEntityDescription( key="minPower", name="min power", device_class=DEVICE_CLASS_POWER, native_unit_of_measurement=POWER_WATT, ), - "maxPower": TibberSensorEntityDescription( + TibberSensorEntityDescription( key="maxPower", name="max power", device_class=DEVICE_CLASS_POWER, native_unit_of_measurement=POWER_WATT, ), - "accumulatedConsumption": TibberSensorEntityDescription( + TibberSensorEntityDescription( key="accumulatedConsumption", name="accumulated consumption", device_class=DEVICE_CLASS_ENERGY, @@ -107,7 +105,7 @@ RT_SENSOR_MAP: dict[str, TibberSensorEntityDescription] = { state_class=STATE_CLASS_MEASUREMENT, reset_type=ResetType.DAILY, ), - "accumulatedConsumptionLastHour": TibberSensorEntityDescription( + TibberSensorEntityDescription( key="accumulatedConsumptionLastHour", name="accumulated consumption current hour", device_class=DEVICE_CLASS_ENERGY, @@ -115,7 +113,7 @@ RT_SENSOR_MAP: dict[str, TibberSensorEntityDescription] = { state_class=STATE_CLASS_MEASUREMENT, reset_type=ResetType.HOURLY, ), - "accumulatedProduction": TibberSensorEntityDescription( + TibberSensorEntityDescription( key="accumulatedProduction", name="accumulated production", device_class=DEVICE_CLASS_ENERGY, @@ -123,7 +121,7 @@ RT_SENSOR_MAP: dict[str, TibberSensorEntityDescription] = { state_class=STATE_CLASS_MEASUREMENT, reset_type=ResetType.DAILY, ), - "accumulatedProductionLastHour": TibberSensorEntityDescription( + TibberSensorEntityDescription( key="accumulatedProductionLastHour", name="accumulated production current hour", device_class=DEVICE_CLASS_ENERGY, @@ -131,7 +129,7 @@ RT_SENSOR_MAP: dict[str, TibberSensorEntityDescription] = { state_class=STATE_CLASS_MEASUREMENT, reset_type=ResetType.HOURLY, ), - "lastMeterConsumption": TibberSensorEntityDescription( + TibberSensorEntityDescription( key="lastMeterConsumption", name="last meter consumption", device_class=DEVICE_CLASS_ENERGY, @@ -139,7 +137,7 @@ RT_SENSOR_MAP: dict[str, TibberSensorEntityDescription] = { state_class=STATE_CLASS_MEASUREMENT, reset_type=ResetType.NEVER, ), - "lastMeterProduction": TibberSensorEntityDescription( + TibberSensorEntityDescription( key="lastMeterProduction", name="last meter production", device_class=DEVICE_CLASS_ENERGY, @@ -147,77 +145,77 @@ RT_SENSOR_MAP: dict[str, TibberSensorEntityDescription] = { state_class=STATE_CLASS_MEASUREMENT, reset_type=ResetType.NEVER, ), - "voltagePhase1": TibberSensorEntityDescription( + TibberSensorEntityDescription( key="voltagePhase1", name="voltage phase1", device_class=DEVICE_CLASS_VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), - "voltagePhase2": TibberSensorEntityDescription( + TibberSensorEntityDescription( key="voltagePhase2", name="voltage phase2", device_class=DEVICE_CLASS_VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), - "voltagePhase3": TibberSensorEntityDescription( + TibberSensorEntityDescription( key="voltagePhase3", name="voltage phase3", device_class=DEVICE_CLASS_VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), - "currentL1": TibberSensorEntityDescription( + TibberSensorEntityDescription( key="currentL1", name="current L1", device_class=DEVICE_CLASS_CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), - "currentL2": TibberSensorEntityDescription( + TibberSensorEntityDescription( key="currentL2", name="current L2", device_class=DEVICE_CLASS_CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), - "currentL3": TibberSensorEntityDescription( + TibberSensorEntityDescription( key="currentL3", name="current L3", device_class=DEVICE_CLASS_CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), - "signalStrength": TibberSensorEntityDescription( + TibberSensorEntityDescription( key="signalStrength", name="signal strength", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, state_class=STATE_CLASS_MEASUREMENT, ), - "accumulatedReward": TibberSensorEntityDescription( + TibberSensorEntityDescription( key="accumulatedReward", name="accumulated reward", device_class=DEVICE_CLASS_MONETARY, state_class=STATE_CLASS_MEASUREMENT, reset_type=ResetType.DAILY, ), - "accumulatedCost": TibberSensorEntityDescription( + TibberSensorEntityDescription( key="accumulatedCost", name="accumulated cost", device_class=DEVICE_CLASS_MONETARY, state_class=STATE_CLASS_MEASUREMENT, reset_type=ResetType.DAILY, ), - "powerFactor": TibberSensorEntityDescription( + TibberSensorEntityDescription( key="powerFactor", name="power factor", device_class=DEVICE_CLASS_POWER_FACTOR, native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, ), -} +) async def async_setup_entry(hass, entry, async_add_entities): @@ -243,7 +241,9 @@ async def async_setup_entry(hass, entry, async_add_entities): entities.append(TibberSensorElPrice(home)) if home.has_real_time_consumption: await home.rt_subscribe( - TibberRtDataHandler(async_add_entities, home, hass).async_callback + TibberRtDataCoordinator( + async_add_entities, home, hass + ).async_set_updated_data ) # migrate @@ -273,27 +273,23 @@ async def async_setup_entry(hass, entry, async_add_entities): class TibberSensor(SensorEntity): """Representation of a generic Tibber sensor.""" - def __init__(self, tibber_home): + def __init__(self, *args, tibber_home, **kwargs): """Initialize the sensor.""" + super().__init__(*args, **kwargs) self._tibber_home = tibber_home self._home_name = tibber_home.info["viewer"]["home"]["appNickname"] - self._device_name = None if self._home_name is None: self._home_name = tibber_home.info["viewer"]["home"]["address"].get( "address1", "" ) + self._device_name = None self._model = None - @property - def device_id(self): - """Return the ID of the physical device this sensor is part of.""" - return self._tibber_home.home_id - @property def device_info(self): """Return the device_info of the device.""" device_info = { - "identifiers": {(TIBBER_DOMAIN, self.device_id)}, + "identifiers": {(TIBBER_DOMAIN, self._tibber_home.home_id)}, "name": self._device_name, "manufacturer": MANUFACTURER, } @@ -307,7 +303,7 @@ class TibberSensorElPrice(TibberSensor): def __init__(self, tibber_home): """Initialize the sensor.""" - super().__init__(tibber_home) + super().__init__(tibber_home=tibber_home) self._last_updated = None self._spread_load_constant = randrange(5000) @@ -377,10 +373,9 @@ class TibberSensorElPrice(TibberSensor): ]["estimatedAnnualConsumption"] -class TibberSensorRT(TibberSensor): +class TibberSensorRT(TibberSensor, update_coordinator.CoordinatorEntity): """Representation of a Tibber sensor for real time consumption.""" - _attr_should_poll = False entity_description: TibberSensorEntityDescription def __init__( @@ -388,9 +383,10 @@ class TibberSensorRT(TibberSensor): tibber_home, description: TibberSensorEntityDescription, initial_state, + coordinator: TibberRtDataCoordinator, ): """Initialize the sensor.""" - super().__init__(tibber_home) + super().__init__(coordinator=coordinator, tibber_home=tibber_home) self.entity_description = description self._model = "Tibber Pulse" self._device_name = f"{self._model} {self._home_name}" @@ -399,7 +395,7 @@ class TibberSensorRT(TibberSensor): self._attr_native_value = initial_state self._attr_unique_id = f"{self._tibber_home.home_id}_rt_{description.name}" - if description.name in ("accumulated cost", "accumulated reward"): + if description.key in ("accumulatedCost", "accumulatedReward"): self._attr_native_unit_of_measurement = tibber_home.currency if description.reset_type == ResetType.NEVER: self._attr_last_reset = dt_util.utc_from_timestamp(0) @@ -414,43 +410,35 @@ class TibberSensorRT(TibberSensor): else: self._attr_last_reset = None - async def async_added_to_hass(self): - """Start listen for real time data.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, - SIGNAL_UPDATE_ENTITY.format(self.unique_id), - self._set_state, - ) - ) - @property def available(self): """Return True if entity is available.""" return self._tibber_home.rt_subscription_running @callback - def _set_state(self, state, timestamp): - """Set sensor state.""" - if ( - state < self._attr_native_value - and self.entity_description.reset_type == ResetType.DAILY - ): - self._attr_last_reset = dt_util.as_utc( - timestamp.replace(hour=0, minute=0, second=0, microsecond=0) - ) - if ( - state < self._attr_native_value - and self.entity_description.reset_type == ResetType.HOURLY - ): - self._attr_last_reset = dt_util.as_utc( - timestamp.replace(minute=0, second=0, microsecond=0) - ) + def _handle_coordinator_update(self) -> None: + if not (live_measurement := self.coordinator.get_live_measurement()): # type: ignore[attr-defined] + return + state = live_measurement.get(self.entity_description.key) + if state is None: + return + timestamp = dt_util.parse_datetime(live_measurement["timestamp"]) + if timestamp is not None and state < self.state: + if self.entity_description.reset_type == ResetType.DAILY: + self._attr_last_reset = dt_util.as_utc( + timestamp.replace(hour=0, minute=0, second=0, microsecond=0) + ) + elif self.entity_description.reset_type == ResetType.HOURLY: + self._attr_last_reset = dt_util.as_utc( + timestamp.replace(minute=0, second=0, microsecond=0) + ) + if self.entity_description.key == "powerFactor": + state *= 100.0 self._attr_native_value = state self.async_write_ha_state() -class TibberRtDataHandler: +class TibberRtDataCoordinator(update_coordinator.DataUpdateCoordinator): """Handle Tibber realtime data.""" def __init__(self, async_add_entities, tibber_home, hass): @@ -458,42 +446,53 @@ class TibberRtDataHandler: self._async_add_entities = async_add_entities self._tibber_home = tibber_home self.hass = hass - self._entities = {} + self._added_sensors = set() + super().__init__( + hass, + _LOGGER, + name=tibber_home.info["viewer"]["home"]["address"].get( + "address1", "Tibber" + ), + ) - async def async_callback(self, payload): - """Handle received data.""" - errors = payload.get("errors") - if errors: - _LOGGER.error(errors[0]) - return - data = payload.get("data") - if data is None: - return - live_measurement = data.get("liveMeasurement") - if live_measurement is None: + self._async_remove_device_updates_handler = self.async_add_listener( + self._add_sensors + ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) + + @callback + def _handle_ha_stop(self, _event) -> None: + """Handle Home Assistant stopping.""" + self._async_remove_device_updates_handler() + + @callback + def _add_sensors(self): + """Add sensor.""" + if not (live_measurement := self.get_live_measurement()): return - timestamp = dt_util.parse_datetime(live_measurement.pop("timestamp")) new_entities = [] - for sensor_type, state in live_measurement.items(): - if state is None or sensor_type not in RT_SENSOR_MAP: + for sensor_description in RT_SENSORS: + if sensor_description.key in self._added_sensors: continue - if sensor_type == "powerFactor": - state *= 100.0 - if sensor_type in self._entities: - async_dispatcher_send( - self.hass, - SIGNAL_UPDATE_ENTITY.format(self._entities[sensor_type]), - state, - timestamp, - ) - else: - entity = TibberSensorRT( - self._tibber_home, - RT_SENSOR_MAP[sensor_type], - state, - ) - new_entities.append(entity) - self._entities[sensor_type] = entity.unique_id + state = live_measurement.get(sensor_description.key) + if state is None: + continue + entity = TibberSensorRT( + self._tibber_home, + sensor_description, + state, + self, + ) + new_entities.append(entity) + self._added_sensors.add(sensor_description.key) if new_entities: self._async_add_entities(new_entities) + + def get_live_measurement(self): + """Get live measurement data.""" + errors = self.data.get("errors") + if errors: + _LOGGER.error(errors[0]) + return None + return self.data.get("data", {}).get("liveMeasurement") From c937a235e15107deb94787e6910e97718abad7c6 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 18 Aug 2021 11:24:15 +0200 Subject: [PATCH 300/355] Add select platform for Xiaomi Miio fans (#54702) * Add select platform for Xiaomi Miio purifiers * Add missing condition for AirFresh * Suggested change * Remove fan_set_led_brightness from services.yaml * Remove zhimi.airpurifier.v3 --- .../components/xiaomi_miio/__init__.py | 2 +- homeassistant/components/xiaomi_miio/const.py | 11 +-- homeassistant/components/xiaomi_miio/fan.py | 65 +--------------- .../components/xiaomi_miio/select.py | 75 +++++++++++++++++-- .../components/xiaomi_miio/services.yaml | 18 ----- 5 files changed, 73 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 122c42c6589..9d854607213 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -46,7 +46,7 @@ _LOGGER = logging.getLogger(__name__) GATEWAY_PLATFORMS = ["alarm_control_panel", "light", "sensor", "switch"] SWITCH_PLATFORMS = ["switch"] -FAN_PLATFORMS = ["fan", "sensor"] +FAN_PLATFORMS = ["fan", "select", "sensor"] HUMIDIFIER_PLATFORMS = [ "binary_sensor", "humidifier", diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index de1c0bcf007..184629fa2fb 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -159,10 +159,8 @@ SERVICE_SET_BUZZER_OFF = "fan_set_buzzer_off" SERVICE_SET_FAN_LED_ON = "fan_set_led_on" SERVICE_SET_FAN_LED_OFF = "fan_set_led_off" SERVICE_SET_FAN_LED = "fan_set_led" -SERVICE_SET_LED_BRIGHTNESS = "set_led_brightness" SERVICE_SET_CHILD_LOCK_ON = "fan_set_child_lock_on" SERVICE_SET_CHILD_LOCK_OFF = "fan_set_child_lock_off" -SERVICE_SET_LED_BRIGHTNESS = "fan_set_led_brightness" SERVICE_SET_FAVORITE_LEVEL = "fan_set_favorite_level" SERVICE_SET_FAN_LEVEL = "fan_set_fan_level" SERVICE_SET_AUTO_DETECT_ON = "fan_set_auto_detect_on" @@ -226,7 +224,6 @@ FEATURE_FLAGS_AIRPURIFIER = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK | FEATURE_SET_LED - | FEATURE_SET_LED_BRIGHTNESS | FEATURE_SET_FAVORITE_LEVEL | FEATURE_SET_LEARN_MODE | FEATURE_RESET_FILTER @@ -261,7 +258,6 @@ FEATURE_FLAGS_AIRPURIFIER_3 = ( | FEATURE_SET_LED | FEATURE_SET_FAVORITE_LEVEL | FEATURE_SET_FAN_LEVEL - | FEATURE_SET_LED_BRIGHTNESS ) FEATURE_FLAGS_AIRPURIFIER_V3 = ( @@ -269,10 +265,7 @@ FEATURE_FLAGS_AIRPURIFIER_V3 = ( ) FEATURE_FLAGS_AIRHUMIDIFIER = ( - FEATURE_SET_BUZZER - | FEATURE_SET_CHILD_LOCK - | FEATURE_SET_LED_BRIGHTNESS - | FEATURE_SET_TARGET_HUMIDITY + FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK | FEATURE_SET_TARGET_HUMIDITY ) FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB = FEATURE_FLAGS_AIRHUMIDIFIER | FEATURE_SET_DRY @@ -284,7 +277,6 @@ FEATURE_FLAGS_AIRHUMIDIFIER_MJSSQ = ( FEATURE_FLAGS_AIRHUMIDIFIER_CA4 = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK - | FEATURE_SET_LED_BRIGHTNESS | FEATURE_SET_TARGET_HUMIDITY | FEATURE_SET_DRY | FEATURE_SET_MOTOR_SPEED @@ -295,7 +287,6 @@ FEATURE_FLAGS_AIRFRESH = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK | FEATURE_SET_LED - | FEATURE_SET_LED_BRIGHTNESS | FEATURE_RESET_FILTER | FEATURE_SET_EXTRA_FEATURES ) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 5b3418c83f5..35c3765d985 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -4,18 +4,9 @@ from enum import Enum import logging import math -from miio.airfresh import ( - LedBrightness as AirfreshLedBrightness, - OperationMode as AirfreshOperationMode, -) -from miio.airpurifier import ( - LedBrightness as AirpurifierLedBrightness, - OperationMode as AirpurifierOperationMode, -) -from miio.airpurifier_miot import ( - LedBrightness as AirpurifierMiotLedBrightness, - OperationMode as AirpurifierMiotOperationMode, -) +from miio.airfresh import OperationMode as AirfreshOperationMode +from miio.airpurifier import OperationMode as AirpurifierOperationMode +from miio.airpurifier_miot import OperationMode as AirpurifierMiotOperationMode import voluptuous as vol from homeassistant.components.fan import ( @@ -52,7 +43,6 @@ from .const import ( FEATURE_SET_FAVORITE_LEVEL, FEATURE_SET_LEARN_MODE, FEATURE_SET_LED, - FEATURE_SET_LED_BRIGHTNESS, FEATURE_SET_VOLUME, KEY_COORDINATOR, KEY_DEVICE, @@ -77,7 +67,6 @@ from .const import ( SERVICE_SET_FAVORITE_LEVEL, SERVICE_SET_LEARN_MODE_OFF, SERVICE_SET_LEARN_MODE_ON, - SERVICE_SET_LED_BRIGHTNESS, SERVICE_SET_VOLUME, ) from .device import XiaomiCoordinatedMiioEntity @@ -107,7 +96,6 @@ ATTR_FAVORITE_LEVEL = "favorite_level" ATTR_BUZZER = "buzzer" ATTR_CHILD_LOCK = "child_lock" ATTR_LED = "led" -ATTR_LED_BRIGHTNESS = "led_brightness" ATTR_BRIGHTNESS = "brightness" ATTR_LEVEL = "level" ATTR_FAN_LEVEL = "fan_level" @@ -142,7 +130,6 @@ AVAILABLE_ATTRIBUTES_AIRPURIFIER = { ATTR_AUTO_DETECT: "auto_detect", ATTR_USE_TIME: "use_time", ATTR_BUZZER: "buzzer", - ATTR_LED_BRIGHTNESS: "led_brightness", ATTR_SLEEP_MODE: "sleep_mode", } @@ -172,7 +159,6 @@ AVAILABLE_ATTRIBUTES_AIRPURIFIER_3 = { ATTR_LED: "led", ATTR_USE_TIME: "use_time", ATTR_BUZZER: "buzzer", - ATTR_LED_BRIGHTNESS: "led_brightness", ATTR_FAN_LEVEL: "fan_level", } @@ -195,7 +181,6 @@ AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 = { AVAILABLE_ATTRIBUTES_AIRFRESH = { ATTR_MODE: "mode", ATTR_LED: "led", - ATTR_LED_BRIGHTNESS: "led_brightness", ATTR_BUZZER: "buzzer", ATTR_CHILD_LOCK: "child_lock", ATTR_USE_TIME: "use_time", @@ -236,7 +221,6 @@ FEATURE_FLAGS_AIRPURIFIER = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK | FEATURE_SET_LED - | FEATURE_SET_LED_BRIGHTNESS | FEATURE_SET_FAVORITE_LEVEL | FEATURE_SET_LEARN_MODE | FEATURE_RESET_FILTER @@ -271,7 +255,6 @@ FEATURE_FLAGS_AIRPURIFIER_3 = ( | FEATURE_SET_LED | FEATURE_SET_FAVORITE_LEVEL | FEATURE_SET_FAN_LEVEL - | FEATURE_SET_LED_BRIGHTNESS ) FEATURE_FLAGS_AIRPURIFIER_V3 = ( @@ -282,17 +265,12 @@ FEATURE_FLAGS_AIRFRESH = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK | FEATURE_SET_LED - | FEATURE_SET_LED_BRIGHTNESS | FEATURE_RESET_FILTER | FEATURE_SET_EXTRA_FEATURES ) AIRPURIFIER_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) -SERVICE_SCHEMA_LED_BRIGHTNESS = AIRPURIFIER_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_BRIGHTNESS): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=2))} -) - SERVICE_SCHEMA_FAVORITE_LEVEL = AIRPURIFIER_SERVICE_SCHEMA.extend( {vol.Required(ATTR_LEVEL): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=17))} ) @@ -321,10 +299,6 @@ SERVICE_TO_METHOD = { SERVICE_SET_LEARN_MODE_ON: {"method": "async_set_learn_mode_on"}, SERVICE_SET_LEARN_MODE_OFF: {"method": "async_set_learn_mode_off"}, SERVICE_RESET_FILTER: {"method": "async_reset_filter"}, - SERVICE_SET_LED_BRIGHTNESS: { - "method": "async_set_led_brightness", - "schema": SERVICE_SCHEMA_LED_BRIGHTNESS, - }, SERVICE_SET_FAVORITE_LEVEL: { "method": "async_set_favorite_level", "schema": SERVICE_SCHEMA_FAVORITE_LEVEL, @@ -792,17 +766,6 @@ class XiaomiAirPurifier(XiaomiGenericDevice): False, ) - async def async_set_led_brightness(self, brightness: int = 2): - """Set the led brightness.""" - if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: - return - - await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, - AirpurifierLedBrightness(brightness), - ) - async def async_set_favorite_level(self, level: int = 1): """Set the favorite level.""" if self._device_features & FEATURE_SET_FAVORITE_LEVEL == 0: @@ -987,17 +950,6 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): self._mode = AirpurifierMiotOperationMode[speed.title()].value self.async_write_ha_state() - async def async_set_led_brightness(self, brightness: int = 2): - """Set the led brightness.""" - if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: - return - - await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, - AirpurifierMiotLedBrightness(brightness), - ) - class XiaomiAirFresh(XiaomiGenericDevice): """Representation of a Xiaomi Air Fresh.""" @@ -1134,17 +1086,6 @@ class XiaomiAirFresh(XiaomiGenericDevice): False, ) - async def async_set_led_brightness(self, brightness: int = 2): - """Set the led brightness.""" - if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: - return - - await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, - AirfreshLedBrightness(brightness), - ) - async def async_set_extra_features(self, features: int = 1): """Set the extra features.""" if self._device_features & FEATURE_SET_EXTRA_FEATURES == 0: diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index 63fa4e069bf..9cb57e5d3d8 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -4,8 +4,11 @@ from __future__ import annotations from dataclasses import dataclass from enum import Enum +from miio.airfresh import LedBrightness as AirfreshLedBrightness from miio.airhumidifier import LedBrightness as AirhumidifierLedBrightness from miio.airhumidifier_miot import LedBrightness as AirhumidifierMiotLedBrightness +from miio.airpurifier import LedBrightness as AirpurifierLedBrightness +from miio.airpurifier_miot import LedBrightness as AirpurifierMiotLedBrightness from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.core import callback @@ -18,8 +21,12 @@ from .const import ( FEATURE_SET_LED_BRIGHTNESS, KEY_COORDINATOR, KEY_DEVICE, + MODEL_AIRFRESH_VA2, + MODEL_AIRPURIFIER_M1, + MODEL_AIRPURIFIER_M2, MODELS_HUMIDIFIER_MIIO, MODELS_HUMIDIFIER_MIOT, + MODELS_PURIFIER_MIOT, ) from .device import XiaomiCoordinatedMiioEntity @@ -27,10 +34,10 @@ ATTR_LED_BRIGHTNESS = "led_brightness" LED_BRIGHTNESS_MAP = {"Bright": 0, "Dim": 1, "Off": 2} -LED_BRIGHTNESS_MAP_MIOT = {"Bright": 2, "Dim": 1, "Off": 0} +LED_BRIGHTNESS_MAP_HUMIDIFIER_MIOT = {"Bright": 2, "Dim": 1, "Off": 0} LED_BRIGHTNESS_REVERSE_MAP = {val: key for key, val in LED_BRIGHTNESS_MAP.items()} -LED_BRIGHTNESS_REVERSE_MAP_MIOT = { - val: key for key, val in LED_BRIGHTNESS_MAP_MIOT.items() +LED_BRIGHTNESS_REVERSE_MAP_HUMIDIFIER_MIOT = { + val: key for key, val in LED_BRIGHTNESS_MAP_HUMIDIFIER_MIOT.items() } @@ -65,6 +72,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entity_class = XiaomiAirHumidifierSelector elif model in MODELS_HUMIDIFIER_MIOT: entity_class = XiaomiAirHumidifierMiotSelector + elif model in [MODEL_AIRPURIFIER_M1, MODEL_AIRPURIFIER_M2]: + entity_class = XiaomiAirPurifierSelector + elif model in MODELS_PURIFIER_MIOT: + entity_class = XiaomiAirPurifierMiotSelector + elif model == MODEL_AIRFRESH_VA2: + entity_class = XiaomiAirFreshSelector else: return @@ -150,14 +163,62 @@ class XiaomiAirHumidifierMiotSelector(XiaomiAirHumidifierSelector): @property def led_brightness(self): """Return the current led brightness.""" - return LED_BRIGHTNESS_REVERSE_MAP_MIOT.get(self._current_led_brightness) + return LED_BRIGHTNESS_REVERSE_MAP_HUMIDIFIER_MIOT.get( + self._current_led_brightness + ) - async def async_set_led_brightness(self, brightness: str): + async def async_set_led_brightness(self, brightness: str) -> None: """Set the led brightness.""" if await self._try_command( "Setting the led brightness of the miio device failed.", self._device.set_led_brightness, - AirhumidifierMiotLedBrightness(LED_BRIGHTNESS_MAP_MIOT[brightness]), + AirhumidifierMiotLedBrightness( + LED_BRIGHTNESS_MAP_HUMIDIFIER_MIOT[brightness] + ), ): - self._current_led_brightness = LED_BRIGHTNESS_MAP_MIOT[brightness] + self._current_led_brightness = LED_BRIGHTNESS_MAP_HUMIDIFIER_MIOT[ + brightness + ] + self.async_write_ha_state() + + +class XiaomiAirPurifierSelector(XiaomiAirHumidifierSelector): + """Representation of a Xiaomi Air Purifier (MIIO protocol) selector.""" + + async def async_set_led_brightness(self, brightness: str) -> None: + """Set the led brightness.""" + if await self._try_command( + "Setting the led brightness of the miio device failed.", + self._device.set_led_brightness, + AirpurifierLedBrightness(LED_BRIGHTNESS_MAP[brightness]), + ): + self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness] + self.async_write_ha_state() + + +class XiaomiAirPurifierMiotSelector(XiaomiAirHumidifierSelector): + """Representation of a Xiaomi Air Purifier (MiOT protocol) selector.""" + + async def async_set_led_brightness(self, brightness: str) -> None: + """Set the led brightness.""" + if await self._try_command( + "Setting the led brightness of the miio device failed.", + self._device.set_led_brightness, + AirpurifierMiotLedBrightness(LED_BRIGHTNESS_MAP[brightness]), + ): + self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness] + self.async_write_ha_state() + + +class XiaomiAirFreshSelector(XiaomiAirHumidifierSelector): + """Representation of a Xiaomi Air Fresh selector.""" + + async def async_set_led_brightness(self, brightness: str) -> None: + """Set the led brightness.""" + if await self._try_command( + "Setting the led brightness of the miio device failed.", + self._device.set_led_brightness, + AirfreshLedBrightness(LED_BRIGHTNESS_MAP[brightness]), + ): + self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness] self.async_write_ha_state() diff --git a/homeassistant/components/xiaomi_miio/services.yaml b/homeassistant/components/xiaomi_miio/services.yaml index 4c153292d7e..43300f8381a 100644 --- a/homeassistant/components/xiaomi_miio/services.yaml +++ b/homeassistant/components/xiaomi_miio/services.yaml @@ -101,24 +101,6 @@ fan_set_fan_level: min: 1 max: 3 -fan_set_led_brightness: - name: Fan set LED brightness - description: Set the led brightness. - fields: - entity_id: - description: Name of the xiaomi miio entity. - selector: - entity: - integration: xiaomi_miio - domain: fan - brightness: - description: Brightness (0 = Bright, 1 = Dim, 2 = Off) - required: true - selector: - number: - min: 0 - max: 2 - fan_set_auto_detect_on: name: Fan set auto detect on description: Turn the auto detect on. From 62015f5495683d8daa3bf0dab74101788cdad13e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Aug 2021 05:13:59 -0500 Subject: [PATCH 301/355] Bump async-upnp-client to 0.20.0, adapt to breaking changes (#54782) --- .../components/dlna_dmr/manifest.json | 2 +- homeassistant/components/ssdp/__init__.py | 26 ++++++------------- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ssdp/test_init.py | 6 ++--- 8 files changed, 17 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 1975128a8cc..67d9713628a 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -2,7 +2,7 @@ "domain": "dlna_dmr", "name": "DLNA Digital Media Renderer", "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.19.2"], + "requirements": ["async-upnp-client==0.20.0"], "dependencies": ["network"], "codeowners": [], "iot_class": "local_push" diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 96bf47d920d..4d21fdb6aab 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -223,25 +223,17 @@ class Scanner: return sources - @core_callback - def async_scan(self, *_: Any) -> None: - """Scan for new entries.""" + async def async_scan(self, *_: Any) -> None: + """Scan for new entries using ssdp default and broadcast target.""" for listener in self._ssdp_listeners: listener.async_search() - - self.async_scan_broadcast() - - @core_callback - def async_scan_broadcast(self, *_: Any) -> None: - """Scan for new entries using broadcast target.""" - # Some sonos devices only seem to respond if we send to the broadcast - # address. This matches pysonos' behavior - # https://github.com/amelchio/pysonos/blob/d4329b4abb657d106394ae69357805269708c996/pysonos/discovery.py#L120 - for listener in self._ssdp_listeners: try: IPv4Address(listener.source_ip) except ValueError: continue + # Some sonos devices only seem to respond if we send to the broadcast + # address. This matches pysonos' behavior + # https://github.com/amelchio/pysonos/blob/d4329b4abb657d106394ae69357805269708c996/pysonos/discovery.py#L120 listener.async_search((str(IPV4_BROADCAST), SSDP_PORT)) async def async_start(self) -> None: @@ -251,7 +243,9 @@ class Scanner: for source_ip in await self._async_build_source_set(): self._ssdp_listeners.append( SSDPListener( - async_callback=self._async_process_entry, source_ip=source_ip + async_connect_callback=self.async_scan, + async_callback=self._async_process_entry, + source_ip=source_ip, ) ) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) @@ -277,10 +271,6 @@ class Scanner: self.hass, self.async_scan, SCAN_INTERVAL ) - # Trigger a broadcast-scan. Regular scan is implicitly triggered - # by SSDPListener. - self.async_scan_broadcast() - @core_callback def _async_get_matching_callbacks( self, headers: Mapping[str, str] diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index ef4b92b4a14..746e90c7388 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/ssdp", "requirements": [ "defusedxml==0.7.1", - "async-upnp-client==0.19.2" + "async-upnp-client==0.20.0" ], "dependencies": ["network"], "after_dependencies": ["zeroconf"], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index fc8ba185d3c..5f38a827ec7 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.19.2"], + "requirements": ["async-upnp-client==0.20.0"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman","@ehendrix23"], "ssdp": [ diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3729b393470..5a0ecee3512 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.2 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.19.2 +async-upnp-client==0.20.0 async_timeout==3.0.1 attrs==21.2.0 awesomeversion==21.4.0 diff --git a/requirements_all.txt b/requirements_all.txt index 0c2674bcb9a..a42251e3882 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -314,7 +314,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp # homeassistant.components.upnp -async-upnp-client==0.19.2 +async-upnp-client==0.20.0 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4575c0c82ab..a31c4616da9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -205,7 +205,7 @@ arcam-fmj==0.7.0 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp # homeassistant.components.upnp -async-upnp-client==0.19.2 +async-upnp-client==0.20.0 # homeassistant.components.aurora auroranoaa==0.0.2 diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 94cf8a58908..2c5dc74db44 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -305,8 +305,8 @@ async def test_start_stop_scanner( async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) await hass.async_block_till_done() assert async_start_mock.call_count == 1 - # Next is 3, as async_upnp_client triggers 1 SSDPListener._async_on_connect - assert async_search_mock.call_count == 3 + # Next is 2, as async_upnp_client triggers 1 SSDPListener._async_on_connect + assert async_search_mock.call_count == 2 assert async_stop_mock.call_count == 0 hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) @@ -314,7 +314,7 @@ async def test_start_stop_scanner( async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) await hass.async_block_till_done() assert async_start_mock.call_count == 1 - assert async_search_mock.call_count == 3 + assert async_search_mock.call_count == 2 assert async_stop_mock.call_count == 1 From cbff6a603d15559eb186c7f4cc4fd32c90799b97 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 12:15:01 +0200 Subject: [PATCH 302/355] Remove unused last_reset from Toon (#54798) --- homeassistant/components/toon/sensor.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py index 8d298c4a865..4522e34943c 100644 --- a/homeassistant/components/toon/sensor.py +++ b/homeassistant/components/toon/sensor.py @@ -1,11 +1,7 @@ """Support for Toon sensors.""" from __future__ import annotations -from homeassistant.components.sensor import ( - ATTR_LAST_RESET, - ATTR_STATE_CLASS, - SensorEntity, -) +from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -127,7 +123,6 @@ class ToonSensor(ToonEntity, SensorEntity): ATTR_DEFAULT_ENABLED, True ) self._attr_icon = sensor.get(ATTR_ICON) - self._attr_last_reset = sensor.get(ATTR_LAST_RESET) self._attr_name = sensor[ATTR_NAME] self._attr_state_class = sensor.get(ATTR_STATE_CLASS) self._attr_native_unit_of_measurement = sensor[ATTR_UNIT_OF_MEASUREMENT] From d1057a70048499c0bc1eec98fbc7b63e646cb218 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 12:17:25 +0200 Subject: [PATCH 303/355] Remove last_reset and update state class for Atome energy (#54801) --- homeassistant/components/atome/sensor.py | 37 +++--------------------- 1 file changed, 4 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 498e760924a..59d193ec8e2 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.const import ( @@ -20,7 +21,7 @@ from homeassistant.const import ( POWER_WATT, ) import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle, dt as dt_util +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -88,16 +89,12 @@ class AtomeData: self._is_connected = None self._day_usage = None self._day_price = None - self._day_last_reset = None self._week_usage = None self._week_price = None - self._week_last_reset = None self._month_usage = None self._month_price = None - self._month_last_reset = None self._year_usage = None self._year_price = None - self._year_last_reset = None @property def live_power(self): @@ -142,11 +139,6 @@ class AtomeData: """Return latest daily usage value.""" return self._day_price - @property - def day_last_reset(self): - """Return latest daily last reset.""" - return self._day_last_reset - @Throttle(DAILY_SCAN_INTERVAL) def update_day_usage(self): """Return current daily power usage.""" @@ -154,7 +146,6 @@ class AtomeData: values = self.atome_client.get_consumption(DAILY_TYPE) self._day_usage = values["total"] / 1000 self._day_price = values["price"] - self._day_last_reset = dt_util.parse_datetime(values["startPeriod"]) _LOGGER.debug("Updating Atome daily data. Got: %d", self._day_usage) except KeyError as error: @@ -170,11 +161,6 @@ class AtomeData: """Return latest weekly usage value.""" return self._week_price - @property - def week_last_reset(self): - """Return latest weekly last reset value.""" - return self._week_last_reset - @Throttle(WEEKLY_SCAN_INTERVAL) def update_week_usage(self): """Return current weekly power usage.""" @@ -182,7 +168,6 @@ class AtomeData: values = self.atome_client.get_consumption(WEEKLY_TYPE) self._week_usage = values["total"] / 1000 self._week_price = values["price"] - self._week_last_reset = dt_util.parse_datetime(values["startPeriod"]) _LOGGER.debug("Updating Atome weekly data. Got: %d", self._week_usage) except KeyError as error: @@ -198,11 +183,6 @@ class AtomeData: """Return latest monthly usage value.""" return self._month_price - @property - def month_last_reset(self): - """Return latest monthly last reset value.""" - return self._month_last_reset - @Throttle(MONTHLY_SCAN_INTERVAL) def update_month_usage(self): """Return current monthly power usage.""" @@ -210,7 +190,6 @@ class AtomeData: values = self.atome_client.get_consumption(MONTHLY_TYPE) self._month_usage = values["total"] / 1000 self._month_price = values["price"] - self._month_last_reset = dt_util.parse_datetime(values["startPeriod"]) _LOGGER.debug("Updating Atome monthly data. Got: %d", self._month_usage) except KeyError as error: @@ -226,11 +205,6 @@ class AtomeData: """Return latest yearly usage value.""" return self._year_price - @property - def year_last_reset(self): - """Return latest yearly last reset value.""" - return self._year_last_reset - @Throttle(YEARLY_SCAN_INTERVAL) def update_year_usage(self): """Return current yearly power usage.""" @@ -238,7 +212,6 @@ class AtomeData: values = self.atome_client.get_consumption(YEARLY_TYPE) self._year_usage = values["total"] / 1000 self._year_price = values["price"] - self._year_last_reset = dt_util.parse_datetime(values["startPeriod"]) _LOGGER.debug("Updating Atome yearly data. Got: %d", self._year_usage) except KeyError as error: @@ -254,14 +227,15 @@ class AtomeSensor(SensorEntity): self._data = data self._sensor_type = sensor_type - self._attr_state_class = STATE_CLASS_MEASUREMENT if sensor_type == LIVE_TYPE: self._attr_device_class = DEVICE_CLASS_POWER self._attr_native_unit_of_measurement = POWER_WATT + self._attr_state_class = STATE_CLASS_MEASUREMENT else: self._attr_device_class = DEVICE_CLASS_ENERGY self._attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR + self._attr_state_class = STATE_CLASS_TOTAL_INCREASING def update(self): """Update device state.""" @@ -276,9 +250,6 @@ class AtomeSensor(SensorEntity): } else: self._attr_native_value = getattr(self._data, f"{self._sensor_type}_usage") - self._attr_last_reset = dt_util.as_utc( - getattr(self._data, f"{self._sensor_type}_last_reset") - ) self._attr_extra_state_attributes = { "price": getattr(self._data, f"{self._sensor_type}_price") } From bf494b569757fb73c06e31b8ea3f072ea6b3b725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 18 Aug 2021 12:31:43 +0200 Subject: [PATCH 304/355] Remove distro from updater requirements (#54804) --- homeassistant/components/updater/manifest.json | 1 - homeassistant/package_constraints.txt | 1 - requirements_all.txt | 3 --- requirements_test_all.txt | 3 --- 4 files changed, 8 deletions(-) diff --git a/homeassistant/components/updater/manifest.json b/homeassistant/components/updater/manifest.json index 9996d2bb1f0..db225bbf242 100644 --- a/homeassistant/components/updater/manifest.json +++ b/homeassistant/components/updater/manifest.json @@ -2,7 +2,6 @@ "domain": "updater", "name": "Updater", "documentation": "https://www.home-assistant.io/integrations/updater", - "requirements": ["distro==1.5.0"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "cloud_polling" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5a0ecee3512..213503a92c5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,6 @@ certifi>=2020.12.5 ciso8601==2.1.3 cryptography==3.3.2 defusedxml==0.7.1 -distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.46.0 home-assistant-frontend==20210813.0 diff --git a/requirements_all.txt b/requirements_all.txt index a42251e3882..0eb8046c327 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -521,9 +521,6 @@ discogs_client==2.3.0 # homeassistant.components.discord discord.py==1.7.2 -# homeassistant.components.updater -distro==1.5.0 - # homeassistant.components.digitalloggers dlipower==0.7.165 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a31c4616da9..bd478a18538 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -302,9 +302,6 @@ devolo-home-control-api==0.17.4 # homeassistant.components.directv directv==0.4.0 -# homeassistant.components.updater -distro==1.5.0 - # homeassistant.components.doorbird doorbirdpy==2.1.0 From 16cb50bddff9788671df4514ad98d634d820e0ee Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 Aug 2021 12:44:35 +0200 Subject: [PATCH 305/355] Ensure device entry in Renault integration (#54797) * Ensure device registry is set even when there are no entities * Fix isort * Use async_get for accessing registry --- .../components/renault/renault_hub.py | 25 ++++++++++- tests/components/renault/__init__.py | 22 +++++++++ tests/components/renault/test_sensor.py | 45 ++++++------------- 3 files changed, 60 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/renault/renault_hub.py b/homeassistant/components/renault/renault_hub.py index 07770ad3769..b7a9b40e2c9 100644 --- a/homeassistant/components/renault/renault_hub.py +++ b/homeassistant/components/renault/renault_hub.py @@ -11,7 +11,15 @@ from renault_api.renault_account import RenaultAccount from renault_api.renault_client import RenaultClient from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ATTR_SW_VERSION, +) from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_KAMEREON_ACCOUNT_ID, DEFAULT_SCAN_INTERVAL @@ -49,11 +57,16 @@ class RenaultHub: self._account = await self._client.get_api_account(account_id) vehicles = await self._account.get_vehicles() + device_registry = dr.async_get(self._hass) if vehicles.vehicleLinks: await asyncio.gather( *( self.async_initialise_vehicle( - vehicle_link, self._account, scan_interval + vehicle_link, + self._account, + scan_interval, + config_entry, + device_registry, ) for vehicle_link in vehicles.vehicleLinks ) @@ -64,6 +77,8 @@ class RenaultHub: vehicle_link: KamereonVehiclesLink, renault_account: RenaultAccount, scan_interval: timedelta, + config_entry: ConfigEntry, + device_registry: dr.DeviceRegistry, ) -> None: """Set up proxy.""" assert vehicle_link.vin is not None @@ -76,6 +91,14 @@ class RenaultHub: scan_interval=scan_interval, ) await vehicle.async_initialise() + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers=vehicle.device_info[ATTR_IDENTIFIERS], + manufacturer=vehicle.device_info[ATTR_MANUFACTURER], + name=vehicle.device_info[ATTR_NAME], + model=vehicle.device_info[ATTR_MODEL], + sw_version=vehicle.device_info[ATTR_SW_VERSION], + ) self._vehicles[vehicle_link.vin] = vehicle async def get_account_ids(self) -> list[str]: diff --git a/tests/components/renault/__init__.py b/tests/components/renault/__init__.py index da72da05d5d..9191851c777 100644 --- a/tests/components/renault/__init__.py +++ b/tests/components/renault/__init__.py @@ -9,8 +9,16 @@ from renault_api.renault_account import RenaultAccount from homeassistant.components.renault.const import DOMAIN from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ATTR_SW_VERSION, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.device_registry import DeviceRegistry from .const import MOCK_CONFIG, MOCK_VEHICLES @@ -218,3 +226,17 @@ async def setup_renault_integration_vehicle_with_side_effect( await hass.async_block_till_done() return config_entry + + +def check_device_registry( + device_registry: DeviceRegistry, expected_device: dict[str, Any] +) -> None: + """Ensure that the expected_device is correctly registered.""" + assert len(device_registry.devices) == 1 + registry_entry = device_registry.async_get_device(expected_device[ATTR_IDENTIFIERS]) + assert registry_entry is not None + assert registry_entry.identifiers == expected_device[ATTR_IDENTIFIERS] + assert registry_entry.manufacturer == expected_device[ATTR_MANUFACTURER] + assert registry_entry.name == expected_device[ATTR_NAME] + assert registry_entry.model == expected_device[ATTR_MODEL] + assert registry_entry.sw_version == expected_device[ATTR_SW_VERSION] diff --git a/tests/components/renault/test_sensor.py b/tests/components/renault/test_sensor.py index 42a75012b38..41fceccb56c 100644 --- a/tests/components/renault/test_sensor.py +++ b/tests/components/renault/test_sensor.py @@ -9,6 +9,7 @@ from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.setup import async_setup_component from . import ( + check_device_registry, setup_renault_integration_vehicle, setup_renault_integration_vehicle_with_no_data, setup_renault_integration_vehicle_with_side_effect, @@ -30,15 +31,7 @@ async def test_sensors(hass, vehicle_type): await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] - assert len(device_registry.devices) == 1 - expected_device = mock_vehicle["expected_device"] - registry_entry = device_registry.async_get_device(expected_device["identifiers"]) - assert registry_entry is not None - assert registry_entry.identifiers == expected_device["identifiers"] - assert registry_entry.manufacturer == expected_device["manufacturer"] - assert registry_entry.name == expected_device["name"] - assert registry_entry.model == expected_device["model"] - assert registry_entry.sw_version == expected_device["sw_version"] + check_device_registry(device_registry, mock_vehicle["expected_device"]) expected_entities = mock_vehicle[SENSOR_DOMAIN] assert len(entity_registry.entities) == len(expected_entities) @@ -65,15 +58,7 @@ async def test_sensor_empty(hass, vehicle_type): await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] - assert len(device_registry.devices) == 1 - expected_device = mock_vehicle["expected_device"] - registry_entry = device_registry.async_get_device(expected_device["identifiers"]) - assert registry_entry is not None - assert registry_entry.identifiers == expected_device["identifiers"] - assert registry_entry.manufacturer == expected_device["manufacturer"] - assert registry_entry.name == expected_device["name"] - assert registry_entry.model == expected_device["model"] - assert registry_entry.sw_version == expected_device["sw_version"] + check_device_registry(device_registry, mock_vehicle["expected_device"]) expected_entities = mock_vehicle[SENSOR_DOMAIN] assert len(entity_registry.entities) == len(expected_entities) @@ -107,15 +92,7 @@ async def test_sensor_errors(hass, vehicle_type): await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] - assert len(device_registry.devices) == 1 - expected_device = mock_vehicle["expected_device"] - registry_entry = device_registry.async_get_device(expected_device["identifiers"]) - assert registry_entry is not None - assert registry_entry.identifiers == expected_device["identifiers"] - assert registry_entry.manufacturer == expected_device["manufacturer"] - assert registry_entry.name == expected_device["name"] - assert registry_entry.model == expected_device["model"] - assert registry_entry.sw_version == expected_device["sw_version"] + check_device_registry(device_registry, mock_vehicle["expected_device"]) expected_entities = mock_vehicle[SENSOR_DOMAIN] assert len(entity_registry.entities) == len(expected_entities) @@ -136,6 +113,7 @@ async def test_sensor_access_denied(hass): entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) + vehicle_type = "zoe_40" access_denied_exception = exceptions.AccessDeniedException( "err.func.403", "Access is denied for this resource", @@ -143,11 +121,13 @@ async def test_sensor_access_denied(hass): with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): await setup_renault_integration_vehicle_with_side_effect( - hass, "zoe_40", access_denied_exception + hass, vehicle_type, access_denied_exception ) await hass.async_block_till_done() - assert len(device_registry.devices) == 0 + mock_vehicle = MOCK_VEHICLES[vehicle_type] + check_device_registry(device_registry, mock_vehicle["expected_device"]) + assert len(entity_registry.entities) == 0 @@ -157,6 +137,7 @@ async def test_sensor_not_supported(hass): entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) + vehicle_type = "zoe_40" not_supported_exception = exceptions.NotSupportedException( "err.tech.501", "This feature is not technically supported by this gateway", @@ -164,9 +145,11 @@ async def test_sensor_not_supported(hass): with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): await setup_renault_integration_vehicle_with_side_effect( - hass, "zoe_40", not_supported_exception + hass, vehicle_type, not_supported_exception ) await hass.async_block_till_done() - assert len(device_registry.devices) == 0 + mock_vehicle = MOCK_VEHICLES[vehicle_type] + check_device_registry(device_registry, mock_vehicle["expected_device"]) + assert len(entity_registry.entities) == 0 From bafbbc6563e674259865c496504dcb126dd90363 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 18 Aug 2021 12:56:54 +0200 Subject: [PATCH 306/355] Adjust modbus constants names (#54792) * Follow up. --- homeassistant/components/modbus/__init__.py | 12 +++--- homeassistant/components/modbus/const.py | 9 +++-- homeassistant/components/modbus/modbus.py | 22 +++++------ tests/components/modbus/conftest.py | 6 +-- tests/components/modbus/test_fan.py | 4 +- tests/components/modbus/test_init.py | 44 ++++++++++----------- tests/components/modbus/test_light.py | 4 +- tests/components/modbus/test_switch.py | 6 +-- 8 files changed, 54 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 8e7d1e48e1a..12e2273bf88 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -73,9 +73,7 @@ from .const import ( CONF_RETRIES, CONF_RETRY_ON_EMPTY, CONF_REVERSE_ORDER, - CONF_RTUOVERTCP, CONF_SCALE, - CONF_SERIAL, CONF_STATE_CLOSED, CONF_STATE_CLOSING, CONF_STATE_OFF, @@ -92,8 +90,6 @@ from .const import ( CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, CONF_TARGET_TEMP, - CONF_TCP, - CONF_UDP, CONF_VERIFY, CONF_WRITE_TYPE, DATA_TYPE_CUSTOM, @@ -114,6 +110,10 @@ from .const import ( DEFAULT_SCAN_INTERVAL, DEFAULT_TEMP_UNIT, MODBUS_DOMAIN as DOMAIN, + RTUOVERTCP, + SERIAL, + TCP, + UDP, ) from .modbus import ModbusHub, async_modbus_setup from .validators import number_validator, scan_interval_validator, struct_validator @@ -304,7 +304,7 @@ MODBUS_SCHEMA = vol.Schema( SERIAL_SCHEMA = MODBUS_SCHEMA.extend( { - vol.Required(CONF_TYPE): CONF_SERIAL, + vol.Required(CONF_TYPE): SERIAL, vol.Required(CONF_BAUDRATE): cv.positive_int, vol.Required(CONF_BYTESIZE): vol.Any(5, 6, 7, 8), vol.Required(CONF_METHOD): vol.Any("rtu", "ascii"), @@ -318,7 +318,7 @@ ETHERNET_SCHEMA = MODBUS_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.port, - vol.Required(CONF_TYPE): vol.Any(CONF_TCP, CONF_UDP, CONF_RTUOVERTCP), + vol.Required(CONF_TYPE): vol.Any(TCP, UDP, RTUOVERTCP), } ) diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index ef6d7c3fc32..01e0fdd5e13 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -39,9 +39,7 @@ CONF_RETRIES = "retries" CONF_RETRY_ON_EMPTY = "retry_on_empty" CONF_REVERSE_ORDER = "reverse_order" CONF_PRECISION = "precision" -CONF_RTUOVERTCP = "rtuovertcp" CONF_SCALE = "scale" -CONF_SERIAL = "serial" CONF_STATE_CLOSED = "state_closed" CONF_STATE_CLOSING = "state_closing" CONF_STATE_OFF = "state_off" @@ -58,13 +56,16 @@ CONF_SWAP_NONE = "none" CONF_SWAP_WORD = "word" CONF_SWAP_WORD_BYTE = "word_byte" CONF_TARGET_TEMP = "target_temp_register" -CONF_TCP = "tcp" -CONF_UDP = "udp" CONF_VERIFY = "verify" CONF_VERIFY_REGISTER = "verify_register" CONF_VERIFY_STATE = "verify_state" CONF_WRITE_TYPE = "write_type" +RTUOVERTCP = "rtuovertcp" +SERIAL = "serial" +TCP = "tcp" +UDP = "udp" + # service call attributes ATTR_ADDRESS = "address" ATTR_HUB = "hub" diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index e2f1295220f..7cab51f7fe6 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -45,16 +45,16 @@ from .const import ( CONF_PARITY, CONF_RETRIES, CONF_RETRY_ON_EMPTY, - CONF_RTUOVERTCP, - CONF_SERIAL, CONF_STOPBITS, - CONF_TCP, - CONF_UDP, DEFAULT_HUB, MODBUS_DOMAIN as DOMAIN, PLATFORMS, + RTUOVERTCP, + SERIAL, SERVICE_WRITE_COIL, SERVICE_WRITE_REGISTER, + TCP, + UDP, ) _LOGGER = logging.getLogger(__name__) @@ -203,10 +203,10 @@ class ModbusHub: self._config_delay = client_config[CONF_DELAY] self._pb_call = {} self._pb_class = { - CONF_SERIAL: ModbusSerialClient, - CONF_TCP: ModbusTcpClient, - CONF_UDP: ModbusUdpClient, - CONF_RTUOVERTCP: ModbusTcpClient, + SERIAL: ModbusSerialClient, + TCP: ModbusTcpClient, + UDP: ModbusUdpClient, + RTUOVERTCP: ModbusTcpClient, } self._pb_params = { "port": client_config[CONF_PORT], @@ -215,7 +215,7 @@ class ModbusHub: "retries": client_config[CONF_RETRIES], "retry_on_empty": client_config[CONF_RETRY_ON_EMPTY], } - if self._config_type == CONF_SERIAL: + if self._config_type == SERIAL: # serial configuration self._pb_params.update( { @@ -229,13 +229,13 @@ class ModbusHub: else: # network configuration self._pb_params["host"] = client_config[CONF_HOST] - if self._config_type == CONF_RTUOVERTCP: + if self._config_type == RTUOVERTCP: self._pb_params["framer"] = ModbusRtuFramer Defaults.Timeout = client_config[CONF_TIMEOUT] if CONF_MSG_WAIT in client_config: self._msg_wait = client_config[CONF_MSG_WAIT] / 1000 - elif self._config_type == CONF_SERIAL: + elif self._config_type == SERIAL: self._msg_wait = 30 / 1000 else: self._msg_wait = 0 diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 4f2c9b2b778..35688d2f608 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -8,9 +8,9 @@ from pymodbus.exceptions import ModbusException import pytest from homeassistant.components.modbus.const import ( - CONF_TCP, DEFAULT_HUB, MODBUS_DOMAIN as DOMAIN, + TCP, ) from homeassistant.const import ( CONF_HOST, @@ -71,7 +71,7 @@ async def mock_modbus(hass, caplog, request, do_config): config = { DOMAIN: [ { - CONF_TYPE: CONF_TCP, + CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, @@ -131,7 +131,7 @@ async def base_test( config_modbus = { DOMAIN: { CONF_NAME: DEFAULT_HUB, - CONF_TYPE: CONF_TCP, + CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, }, diff --git a/tests/components/modbus/test_fan.py b/tests/components/modbus/test_fan.py index 4aa55473737..fb65f737d27 100644 --- a/tests/components/modbus/test_fan.py +++ b/tests/components/modbus/test_fan.py @@ -12,10 +12,10 @@ from homeassistant.components.modbus.const import ( CONF_INPUT_TYPE, CONF_STATE_OFF, CONF_STATE_ON, - CONF_TCP, CONF_VERIFY, CONF_WRITE_TYPE, MODBUS_DOMAIN, + TCP, ) from homeassistant.const import ( CONF_ADDRESS, @@ -219,7 +219,7 @@ async def test_fan_service_turn(hass, caplog, mock_pymodbus): ENTITY_ID2 = f"{FAN_DOMAIN}.{TEST_ENTITY_NAME}2" config = { MODBUS_DOMAIN: { - CONF_TYPE: CONF_TCP, + CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_FANS: [ diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 9400dd56641..3eb1beb460f 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -42,21 +42,21 @@ from homeassistant.components.modbus.const import ( CONF_INPUT_TYPE, CONF_MSG_WAIT, CONF_PARITY, - CONF_RTUOVERTCP, - CONF_SERIAL, CONF_STOPBITS, CONF_SWAP, CONF_SWAP_BYTE, CONF_SWAP_WORD, - CONF_TCP, - CONF_UDP, DATA_TYPE_CUSTOM, DATA_TYPE_INT, DATA_TYPE_STRING, DEFAULT_SCAN_INTERVAL, MODBUS_DOMAIN as DOMAIN, + RTUOVERTCP, + SERIAL, SERVICE_WRITE_COIL, SERVICE_WRITE_REGISTER, + TCP, + UDP, ) from homeassistant.components.modbus.validators import ( number_validator, @@ -206,12 +206,12 @@ async def test_exception_struct_validator(do_config): "do_config", [ { - CONF_TYPE: CONF_TCP, + CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, }, { - CONF_TYPE: CONF_TCP, + CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, @@ -219,12 +219,12 @@ async def test_exception_struct_validator(do_config): CONF_DELAY: 10, }, { - CONF_TYPE: CONF_UDP, + CONF_TYPE: UDP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, }, { - CONF_TYPE: CONF_UDP, + CONF_TYPE: UDP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, @@ -232,12 +232,12 @@ async def test_exception_struct_validator(do_config): CONF_DELAY: 10, }, { - CONF_TYPE: CONF_RTUOVERTCP, + CONF_TYPE: RTUOVERTCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, }, { - CONF_TYPE: CONF_RTUOVERTCP, + CONF_TYPE: RTUOVERTCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, @@ -245,7 +245,7 @@ async def test_exception_struct_validator(do_config): CONF_DELAY: 10, }, { - CONF_TYPE: CONF_SERIAL, + CONF_TYPE: SERIAL, CONF_BAUDRATE: 9600, CONF_BYTESIZE: 8, CONF_METHOD: "rtu", @@ -255,7 +255,7 @@ async def test_exception_struct_validator(do_config): CONF_MSG_WAIT: 100, }, { - CONF_TYPE: CONF_SERIAL, + CONF_TYPE: SERIAL, CONF_BAUDRATE: 9600, CONF_BYTESIZE: 8, CONF_METHOD: "rtu", @@ -267,26 +267,26 @@ async def test_exception_struct_validator(do_config): CONF_DELAY: 10, }, { - CONF_TYPE: CONF_TCP, + CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_DELAY: 5, }, [ { - CONF_TYPE: CONF_TCP, + CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, }, { - CONF_TYPE: CONF_TCP, + CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_NAME: f"{TEST_MODBUS_NAME}2", }, { - CONF_TYPE: CONF_SERIAL, + CONF_TYPE: SERIAL, CONF_BAUDRATE: 9600, CONF_BYTESIZE: 8, CONF_METHOD: "rtu", @@ -298,7 +298,7 @@ async def test_exception_struct_validator(do_config): ], { # Special test for scan_interval validator with scan_interval: 0 - CONF_TYPE: CONF_TCP, + CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_SENSORS: [ @@ -326,7 +326,7 @@ SERVICE = "service" [ { CONF_NAME: TEST_MODBUS_NAME, - CONF_TYPE: CONF_SERIAL, + CONF_TYPE: SERIAL, CONF_BAUDRATE: 9600, CONF_BYTESIZE: 8, CONF_METHOD: "rtu", @@ -431,7 +431,7 @@ async def mock_modbus_read_pymodbus( config = { DOMAIN: [ { - CONF_TYPE: CONF_TCP, + CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, @@ -505,7 +505,7 @@ async def test_pymodbus_constructor_fail(hass, caplog): config = { DOMAIN: [ { - CONF_TYPE: CONF_TCP, + CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, } @@ -528,7 +528,7 @@ async def test_pymodbus_close_fail(hass, caplog, mock_pymodbus): config = { DOMAIN: [ { - CONF_TYPE: CONF_TCP, + CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, } @@ -553,7 +553,7 @@ async def test_delay(hass, mock_pymodbus): config = { DOMAIN: [ { - CONF_TYPE: CONF_TCP, + CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, diff --git a/tests/components/modbus/test_light.py b/tests/components/modbus/test_light.py index 49bfed3e19a..f679883e908 100644 --- a/tests/components/modbus/test_light.py +++ b/tests/components/modbus/test_light.py @@ -11,10 +11,10 @@ from homeassistant.components.modbus.const import ( CONF_INPUT_TYPE, CONF_STATE_OFF, CONF_STATE_ON, - CONF_TCP, CONF_VERIFY, CONF_WRITE_TYPE, MODBUS_DOMAIN, + TCP, ) from homeassistant.const import ( CONF_ADDRESS, @@ -219,7 +219,7 @@ async def test_light_service_turn(hass, caplog, mock_pymodbus): ENTITY_ID2 = f"{ENTITY_ID}2" config = { MODBUS_DOMAIN: { - CONF_TYPE: CONF_TCP, + CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_LIGHTS: [ diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py index 3838e7a95d5..302189001c5 100644 --- a/tests/components/modbus/test_switch.py +++ b/tests/components/modbus/test_switch.py @@ -13,10 +13,10 @@ from homeassistant.components.modbus.const import ( CONF_INPUT_TYPE, CONF_STATE_OFF, CONF_STATE_ON, - CONF_TCP, CONF_VERIFY, CONF_WRITE_TYPE, MODBUS_DOMAIN, + TCP, ) from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( @@ -233,7 +233,7 @@ async def test_switch_service_turn(hass, caplog, mock_pymodbus): ENTITY_ID2 = f"{SWITCH_DOMAIN}.{TEST_ENTITY_NAME}2" config = { MODBUS_DOMAIN: { - CONF_TYPE: CONF_TCP, + CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_SWITCHES: [ @@ -330,7 +330,7 @@ async def test_delay_switch(hass, mock_pymodbus): config = { MODBUS_DOMAIN: [ { - CONF_TYPE: CONF_TCP, + CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_SWITCHES: [ From 1280a38e0f827534bd50fffc2bdfbdfff42575d2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 13:12:37 +0200 Subject: [PATCH 307/355] Remove last_reset attribute from fritz sensors (#54806) --- homeassistant/components/fritz/sensor.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index cbbaa40aaa6..e3d366e83fd 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -8,7 +8,11 @@ from typing import Callable, TypedDict from fritzconnection.core.exceptions import FritzConnectionException from fritzconnection.lib.fritzstatus import FritzStatus -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DATA_GIGABYTES, @@ -134,7 +138,6 @@ class SensorData(TypedDict, total=False): name: str device_class: str | None state_class: str | None - last_reset: bool unit_of_measurement: str | None icon: str | None state_provider: Callable @@ -185,16 +188,14 @@ SENSOR_DATA = { ), "gb_sent": SensorData( name="GB sent", - state_class=STATE_CLASS_MEASUREMENT, - last_reset=True, + state_class=STATE_CLASS_TOTAL_INCREASING, unit_of_measurement=DATA_GIGABYTES, icon="mdi:upload", state_provider=_retrieve_gb_sent_state, ), "gb_received": SensorData( name="GB received", - state_class=STATE_CLASS_MEASUREMENT, - last_reset=True, + state_class=STATE_CLASS_TOTAL_INCREASING, unit_of_measurement=DATA_GIGABYTES, icon="mdi:download", state_provider=_retrieve_gb_received_state, @@ -284,7 +285,6 @@ class FritzBoxSensor(FritzBoxBaseEntity, SensorEntity): """Init FRITZ!Box connectivity class.""" self._sensor_data: SensorData = SENSOR_DATA[sensor_type] self._last_device_value: str | None = None - self._last_wan_value: str | None = None self._attr_available = True self._attr_device_class = self._sensor_data.get("device_class") self._attr_icon = self._sensor_data.get("icon") @@ -316,12 +316,3 @@ class FritzBoxSensor(FritzBoxBaseEntity, SensorEntity): self._attr_native_value = self._last_device_value = self._state_provider( status, self._last_device_value ) - - if self._sensor_data.get("last_reset") is True: - self._last_wan_value = _retrieve_connection_uptime_state( - status, self._last_wan_value - ) - self._attr_last_reset = datetime.datetime.strptime( - self._last_wan_value, - "%Y-%m-%dT%H:%M:%S%z", - ) From dcb2a211e54680a19b4e27206b113ea64f2c2230 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 13:13:35 +0200 Subject: [PATCH 308/355] Remove last_reset attribute and set state class to total_increasing for Shelly energy sensors (#54800) --- homeassistant/components/shelly/const.py | 3 -- homeassistant/components/shelly/entity.py | 1 - homeassistant/components/shelly/sensor.py | 58 +++-------------------- 3 files changed, 7 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index ea6b9320cb1..5646086285d 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -109,6 +109,3 @@ KELVIN_MIN_VALUE_WHITE: Final = 2700 KELVIN_MIN_VALUE_COLOR: Final = 3000 UPTIME_DEVIATION: Final = 5 - -LAST_RESET_UPTIME: Final = "uptime" -LAST_RESET_NEVER: Final = "never" diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 0d23f5abffc..743dd07414e 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -179,7 +179,6 @@ class BlockAttributeDescription: # Callable (settings, block), return true if entity should be removed removal_condition: Callable[[dict, aioshelly.Block], bool] | None = None extra_state_attributes: Callable[[aioshelly.Block], dict | None] | None = None - last_reset: str | None = None @dataclass diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index e3af10571d5..13cf56d3b3d 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -1,12 +1,8 @@ """Sensor for Shelly.""" from __future__ import annotations -from datetime import timedelta -import logging from typing import Final, cast -import aioshelly - from homeassistant.components import sensor from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry @@ -24,10 +20,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from homeassistant.util import dt -from . import ShellyDeviceWrapper -from .const import LAST_RESET_NEVER, LAST_RESET_UPTIME, SHAIR_MAX_WORK_HOURS +from .const import SHAIR_MAX_WORK_HOURS from .entity import ( BlockAttributeDescription, RestAttributeDescription, @@ -39,8 +33,6 @@ from .entity import ( ) from .utils import get_device_uptime, temperature_unit -_LOGGER: Final = logging.getLogger(__name__) - SENSORS: Final = { ("device", "battery"): BlockAttributeDescription( name="Battery", @@ -119,49 +111,43 @@ SENSORS: Final = { unit=ENERGY_KILO_WATT_HOUR, value=lambda value: round(value / 60 / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, - state_class=sensor.STATE_CLASS_MEASUREMENT, - last_reset=LAST_RESET_UPTIME, + state_class=sensor.STATE_CLASS_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_MEASUREMENT, - last_reset=LAST_RESET_NEVER, + state_class=sensor.STATE_CLASS_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_MEASUREMENT, - last_reset=LAST_RESET_NEVER, + state_class=sensor.STATE_CLASS_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_MEASUREMENT, + state_class=sensor.STATE_CLASS_TOTAL_INCREASING, default_enabled=False, - last_reset=LAST_RESET_UPTIME, ), ("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_MEASUREMENT, - last_reset=LAST_RESET_UPTIME, + state_class=sensor.STATE_CLASS_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_MEASUREMENT, - last_reset=LAST_RESET_UPTIME, + state_class=sensor.STATE_CLASS_TOTAL_INCREASING, ), ("sensor", "concentration"): BlockAttributeDescription( name="Gas Concentration", @@ -261,39 +247,9 @@ async def async_setup_entry( class ShellySensor(ShellyBlockAttributeEntity, SensorEntity): """Represent a shelly sensor.""" - def __init__( - self, - wrapper: ShellyDeviceWrapper, - block: aioshelly.Block, - attribute: str, - description: BlockAttributeDescription, - ) -> None: - """Initialize sensor.""" - super().__init__(wrapper, block, attribute, description) - self._last_value: float | None = None - - if description.last_reset == LAST_RESET_NEVER: - self._attr_last_reset = dt.utc_from_timestamp(0) - elif description.last_reset == LAST_RESET_UPTIME: - self._attr_last_reset = ( - dt.utcnow() - timedelta(seconds=wrapper.device.status["uptime"]) - ).replace(second=0, microsecond=0) - @property def native_value(self) -> StateType: """Return value of sensor.""" - if ( - self.description.last_reset == LAST_RESET_UPTIME - and self.attribute_value is not None - ): - value = cast(float, self.attribute_value) - - if self._last_value and self._last_value > value: - self._attr_last_reset = dt.utcnow().replace(second=0, microsecond=0) - _LOGGER.info("Energy reset detected for entity %s", self.name) - - self._last_value = value - return self.attribute_value @property From 939fde0a500f599fac9f9c5cd2befa8b75f45475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 18 Aug 2021 14:22:05 +0300 Subject: [PATCH 309/355] ConfigType and async_setup/setup type hint improvements (#54739) --- homeassistant/components/analytics/__init__.py | 3 ++- homeassistant/components/arcam_fmj/__init__.py | 2 +- homeassistant/components/bmw_connected_drive/__init__.py | 3 ++- homeassistant/components/coronavirus/__init__.py | 3 ++- homeassistant/components/dhcp/__init__.py | 3 ++- homeassistant/components/doorbird/__init__.py | 3 ++- homeassistant/components/dynalite/__init__.py | 3 ++- homeassistant/components/emulated_kasa/__init__.py | 3 ++- homeassistant/components/fan/__init__.py | 3 ++- homeassistant/components/firmata/__init__.py | 3 ++- homeassistant/components/google_assistant/__init__.py | 5 ++--- homeassistant/components/google_pubsub/__init__.py | 4 ++-- homeassistant/components/habitica/__init__.py | 3 ++- homeassistant/components/hdmi_cec/__init__.py | 3 ++- homeassistant/components/heos/__init__.py | 2 +- homeassistant/components/home_connect/__init__.py | 3 ++- homeassistant/components/home_plus_control/__init__.py | 3 ++- homeassistant/components/homeassistant/__init__.py | 3 ++- homeassistant/components/homekit/__init__.py | 3 ++- homeassistant/components/huawei_lte/__init__.py | 2 +- homeassistant/components/image/__init__.py | 3 ++- homeassistant/components/intent/__init__.py | 3 ++- homeassistant/components/izone/__init__.py | 2 +- homeassistant/components/juicenet/__init__.py | 3 ++- homeassistant/components/konnected/__init__.py | 3 ++- homeassistant/components/lovelace/__init__.py | 2 +- homeassistant/components/lyric/__init__.py | 3 ++- homeassistant/components/media_source/__init__.py | 3 ++- homeassistant/components/melcloud/__init__.py | 3 ++- homeassistant/components/mobile_app/__init__.py | 2 +- homeassistant/components/nest/__init__.py | 3 ++- homeassistant/components/netatmo/__init__.py | 3 ++- homeassistant/components/nfandroidtv/__init__.py | 3 ++- homeassistant/components/nzbget/__init__.py | 3 ++- homeassistant/components/onvif/__init__.py | 3 ++- homeassistant/components/persistent_notification/__init__.py | 3 ++- homeassistant/components/person/__init__.py | 2 +- homeassistant/components/plum_lightpad/__init__.py | 3 ++- homeassistant/components/proxmoxve/__init__.py | 3 ++- homeassistant/components/pvpc_hourly_pricing/__init__.py | 3 ++- homeassistant/components/rest/__init__.py | 3 ++- homeassistant/components/safe_mode/__init__.py | 3 ++- homeassistant/components/screenlogic/__init__.py | 3 ++- homeassistant/components/search/__init__.py | 3 ++- homeassistant/components/smappee/__init__.py | 3 ++- homeassistant/components/smarthab/__init__.py | 3 ++- homeassistant/components/smartthings/__init__.py | 2 +- homeassistant/components/songpal/__init__.py | 4 ++-- homeassistant/components/stt/__init__.py | 3 ++- homeassistant/components/surepetcare/__init__.py | 3 ++- homeassistant/components/switcher_kis/__init__.py | 3 ++- homeassistant/components/system_health/__init__.py | 2 +- homeassistant/components/tradfri/__init__.py | 2 +- homeassistant/components/upnp/__init__.py | 2 +- homeassistant/components/vera/__init__.py | 3 ++- homeassistant/components/xbox/__init__.py | 3 ++- homeassistant/components/yeelight/__init__.py | 3 ++- homeassistant/components/zeroconf/__init__.py | 3 ++- homeassistant/components/zone/__init__.py | 3 ++- homeassistant/components/zwave_js/__init__.py | 3 ++- homeassistant/config.py | 2 +- .../templates/config_flow_oauth2/integration/__init__.py | 5 ++--- .../scaffold/templates/integration/integration/__init__.py | 5 ++--- 63 files changed, 114 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/analytics/__init__.py b/homeassistant/components/analytics/__init__.py index d41970a79de..944acc6ef9d 100644 --- a/homeassistant/components/analytics/__init__.py +++ b/homeassistant/components/analytics/__init__.py @@ -5,12 +5,13 @@ from homeassistant.components import websocket_api from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import HomeAssistant from homeassistant.helpers.event import async_call_later, async_track_time_interval +from homeassistant.helpers.typing import ConfigType from .analytics import Analytics from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA -async def async_setup(hass: HomeAssistant, _): +async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool: """Set up the analytics integration.""" analytics = Analytics(hass) diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index c1df4fc0587..d28de3b92aa 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -36,7 +36,7 @@ async def _await_cancel(task): await task -async def async_setup(hass: HomeAssistant, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the component.""" hass.data[DOMAIN_DATA_ENTRIES] = {} hass.data[DOMAIN_DATA_TASKS] = {} diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 17e57b5d09c..85a5c9cd02f 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -23,6 +23,7 @@ from homeassistant.helpers import device_registry, discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_utc_time_change +from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify import homeassistant.util.dt as dt_util @@ -79,7 +80,7 @@ _SERVICE_MAP = { UNDO_UPDATE_LISTENER = "undo_update_listener" -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the BMW Connected Drive component from configuration.yaml.""" hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][DATA_HASS_CONFIG] = config diff --git a/homeassistant/components/coronavirus/__init__.py b/homeassistant/components/coronavirus/__init__.py index c855137fcbf..d130e131c8b 100644 --- a/homeassistant/components/coronavirus/__init__.py +++ b/homeassistant/components/coronavirus/__init__.py @@ -8,13 +8,14 @@ import coronavirus from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client, entity_registry, update_coordinator +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN PLATFORMS = ["sensor"] -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Coronavirus component.""" # Make sure coordinator is initialized. await get_coordinator(hass) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 7003038593b..1a49667bad8 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -41,6 +41,7 @@ from homeassistant.helpers.event import ( async_track_state_added_domain, async_track_time_interval, ) +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_dhcp from homeassistant.util.network import is_invalid, is_link_local, is_loopback @@ -58,7 +59,7 @@ SCAN_INTERVAL = timedelta(minutes=60) _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the dhcp component.""" async def _initialize(_): diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index d5964d5aea0..07366ad1a9a 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -22,6 +22,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.network import get_url +from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util, slugify from .const import ( @@ -58,7 +59,7 @@ DEVICE_SCHEMA = vol.Schema( CONFIG_SCHEMA = cv.deprecated(DOMAIN) -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the DoorBird component.""" hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py index 7dc3d86afe6..49e742519fd 100644 --- a/homeassistant/components/dynalite/__init__.py +++ b/homeassistant/components/dynalite/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import CONF_DEFAULT, CONF_HOST, CONF_NAME, CONF_PORT, C from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType # Loading the config flow file will register the flow from .bridge import DynaliteBridge @@ -179,7 +180,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Dynalite platform.""" conf = config.get(DOMAIN) LOGGER.debug("Setting up dynalite component config = %s", conf) diff --git a/homeassistant/components/emulated_kasa/__init__.py b/homeassistant/components/emulated_kasa/__init__.py index b9dc79e25cc..d513669cd00 100644 --- a/homeassistant/components/emulated_kasa/__init__.py +++ b/homeassistant/components/emulated_kasa/__init__.py @@ -18,6 +18,7 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.template import Template, is_template_string +from homeassistant.helpers.typing import ConfigType from .const import CONF_POWER, CONF_POWER_ENTITY, DOMAIN @@ -48,7 +49,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the emulated_kasa component.""" conf = config.get(DOMAIN) if not conf: diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 1d0caa3231b..a05505e8112 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -25,6 +25,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util.percentage import ( ordered_list_item_to_percentage, @@ -124,7 +125,7 @@ def is_on(hass, entity_id: str) -> bool: return state.state == STATE_ON -async def async_setup(hass, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Expose fan control via statemachine and services.""" component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL diff --git a/homeassistant/components/firmata/__init__.py b/homeassistant/components/firmata/__init__.py index 24b6420e8a5..d98866f900b 100644 --- a/homeassistant/components/firmata/__init__.py +++ b/homeassistant/components/firmata/__init__.py @@ -19,6 +19,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.typing import ConfigType from .board import FirmataBoard from .const import ( @@ -122,7 +123,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Firmata domain.""" # Delete specific entries that no longer exist in the config if hass.config_entries.async_entries(DOMAIN): diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 13516783233..1e0c0a06114 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -2,14 +2,13 @@ from __future__ import annotations import logging -from typing import Any import voluptuous as vol -# Typing imports from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_ALIASES, @@ -91,7 +90,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, yaml_config: dict[str, Any]): +async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: """Activate Google Actions component.""" if DOMAIN not in yaml_config: return True diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index d583bc5aac0..1de7e98d776 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -5,7 +5,6 @@ import datetime import json import logging import os -from typing import Any from google.cloud import pubsub_v1 import voluptuous as vol @@ -14,6 +13,7 @@ from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UN from homeassistant.core import Event, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA +from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -39,7 +39,7 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass: HomeAssistant, yaml_config: dict[str, Any]): +def setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: """Activate Google Pub/Sub component.""" config = yaml_config[DOMAIN] project_id = config[CONF_PROJECT_ID] diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index efb82a9f1aa..1d1536d1679 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( 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.typing import ConfigType from .const import ( ATTR_ARGS, @@ -83,7 +84,7 @@ SERVICE_API_CALL_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Habitica service.""" configs = config.get(DOMAIN, []) diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index 9d4fa286fd6..87391634251 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -45,6 +45,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import discovery, event import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType DOMAIN = "hdmi_cec" @@ -186,7 +187,7 @@ def parse_mapping(mapping, parents=None): yield (val, pad_physical_address(cur)) -def setup(hass: HomeAssistant, base_config): # noqa: C901 +def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: # noqa: C901 """Set up the CEC capability.""" # Parse configuration into a dict of device name to physical address diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index 7490c1e5be1..35520927e97 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -43,7 +43,7 @@ MIN_UPDATE_SOURCES = timedelta(seconds=1) _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the HEOS component.""" if DOMAIN not in config: return True diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index f8a9157dca2..1fc446af401 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -10,6 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv +from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle from . import api, config_flow @@ -34,7 +35,7 @@ CONFIG_SCHEMA = vol.Schema( PLATFORMS = ["binary_sensor", "light", "sensor", "switch"] -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Home Connect component.""" hass.data[DOMAIN] = {} diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py index e775b9d97aa..ffb055e6324 100644 --- a/homeassistant/components/home_plus_control/__init__.py +++ b/homeassistant/components/home_plus_control/__init__.py @@ -16,6 +16,7 @@ from homeassistant.helpers import ( dispatcher, ) from homeassistant.helpers.device_registry import async_get as async_get_device_registry +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from . import config_flow, helpers @@ -50,7 +51,7 @@ PLATFORMS = ["switch"] _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Legrand Home+ Control component from configuration.yaml.""" hass.data[DOMAIN] = {} diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index d21cd1359f1..2314d2b0c1b 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -26,6 +26,7 @@ from homeassistant.helpers.service import ( async_extract_config_entry_ids, async_extract_referenced_entity_ids, ) +from homeassistant.helpers.typing import ConfigType ATTR_ENTRY_ID = "entry_id" @@ -51,7 +52,7 @@ SCHEMA_RELOAD_CONFIG_ENTRY = vol.All( SHUTDOWN_SERVICES = (SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART) -async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: # noqa: C901 +async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # noqa: C901 """Set up general services related to Home Assistant.""" async def async_save_persistent_states(service): diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 967acaf7ddc..705b671f28a 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -44,6 +44,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import BASE_FILTER_SCHEMA, FILTER_SCHEMA from homeassistant.helpers.reload import async_integration_yaml_config from homeassistant.helpers.service import async_extract_referenced_entity_ids +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import IntegrationNotFound, async_get_integration from . import ( # noqa: F401 @@ -187,7 +188,7 @@ def _async_get_entries_by_name(current_entries): return {entry.data.get(CONF_NAME, BRIDGE_NAME): entry for entry in current_entries} -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the HomeKit from yaml.""" hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 0c545486c82..e220975dbf1 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -298,7 +298,7 @@ class Router: class HuaweiLteData: """Shared state.""" - hass_config: dict = attr.ib() + hass_config: ConfigType = attr.ib() # 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) diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py index e27abf70127..51263e38ab7 100644 --- a/homeassistant/components/image/__init__.py +++ b/homeassistant/components/image/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import collection from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util from .const import DOMAIN @@ -37,7 +38,7 @@ UPDATE_FIELDS = { } -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Image integration.""" image_dir = pathlib.Path(hass.config.path(DOMAIN)) hass.data[DOMAIN] = storage_collection = ImageStorageCollection(hass, image_dir) diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index 4fd6daa5102..d626daa8c3b 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -6,11 +6,12 @@ from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.const import SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant from homeassistant.helpers import config_validation as cv, integration_platform, intent +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Intent component.""" hass.http.register_view(IntentHandleView()) diff --git a/homeassistant/components/izone/__init__.py b/homeassistant/components/izone/__init__.py index 76744550649..e3f4b62af63 100644 --- a/homeassistant/components/izone/__init__.py +++ b/homeassistant/components/izone/__init__.py @@ -26,7 +26,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Register the iZone component config.""" conf = config.get(IZONE) if not conf: diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index 38089a6e17f..0480eac80b3 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -12,6 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR @@ -30,7 +31,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the JuiceNet component.""" conf = config.get(DOMAIN) hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 32d0f0e20c0..6785e2e7124 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.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.typing import ConfigType from .config_flow import ( # Loading the config flow file will register the flow CONF_DEFAULT_OPTIONS, @@ -220,7 +221,7 @@ YAML_CONFIGS = "yaml_configs" PLATFORMS = ["binary_sensor", "sensor", "switch"] -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Konnected platform.""" cfg = config.get(DOMAIN) if cfg is None: diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index e16f1399c40..d8fe591a0ba 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -67,7 +67,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Lovelace commands.""" mode = config[DOMAIN][CONF_MODE] yaml_resources = config[DOMAIN].get(CONF_RESOURCES) diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index e958567940a..4afb66f7173 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -23,6 +23,7 @@ from homeassistant.helpers import ( device_registry as dr, ) from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -50,7 +51,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["climate", "sensor"] -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Honeywell Lyric component.""" hass.data[DOMAIN] = {} diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index 5b027a99bf9..cb485ac765f 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -14,6 +14,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, ) +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from . import local_source, models @@ -36,7 +37,7 @@ def generate_media_source_id(domain: str, identifier: str) -> str: return uri -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the media_source component.""" hass.data[DOMAIN] = {} hass.components.websocket_api.async_register_command(websocket_browse_media) diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index 12b80554933..69efa26ac44 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -17,6 +17,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle from .const import DOMAIN @@ -44,7 +45,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: ConfigEntry): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Establish connection with MELCloud.""" if DOMAIN not in config: return True diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 9633ec6556d..1fc5be2a890 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -36,7 +36,7 @@ from .webhook import handle_webhook PLATFORMS = "sensor", "binary_sensor", "device_tracker" -async def async_setup(hass: HomeAssistant, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the mobile app component.""" store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) app_config = await store.async_load() diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index b999b2e94e0..ff340d38424 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -27,6 +27,7 @@ from homeassistant.helpers import ( config_entry_oauth2_flow, config_validation as cv, ) +from homeassistant.helpers.typing import ConfigType from . import api, config_flow from .const import DATA_SDM, DATA_SUBSCRIBER, DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN @@ -69,7 +70,7 @@ CONFIG_SCHEMA = vol.Schema( PLATFORMS = ["sensor", "camera", "climate"] -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Nest components with dispatch between old/new flows.""" hass.data[DOMAIN] = {} diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index edb8837fd18..76a5eeb9c86 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -31,6 +31,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.typing import ConfigType from . import api, config_flow from .const import ( @@ -69,7 +70,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Netatmo component.""" hass.data[DOMAIN] = { DATA_PERSONS: {}, diff --git a/homeassistant/components/nfandroidtv/__init__.py b/homeassistant/components/nfandroidtv/__init__.py index 35aecdb6916..92bb492bf7d 100644 --- a/homeassistant/components/nfandroidtv/__init__.py +++ b/homeassistant/components/nfandroidtv/__init__.py @@ -7,13 +7,14 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PLATFORM from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN PLATFORMS = [NOTIFY] -async def async_setup(hass: HomeAssistant, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the NFAndroidTV component.""" hass.data.setdefault(DOMAIN, {}) # Iterate all entries for notify to only get nfandroidtv diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index 71f885ce491..ebb3a7e4e66 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -13,6 +13,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -60,7 +61,7 @@ SPEED_LIMIT_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the NZBGet integration.""" hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 5c44cdf1750..67bec21e123 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_per_platform +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_RTSP_TRANSPORT, @@ -31,7 +32,7 @@ from .const import ( from .device import ONVIFDevice -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the ONVIF component.""" # Import from yaml configs = {} diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 4a68dd3356f..ec2c5f7512d 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -15,6 +15,7 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.template import Template +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util import slugify import homeassistant.util.dt as dt_util @@ -100,7 +101,7 @@ def async_dismiss(hass: HomeAssistant, notification_id: str) -> None: hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_DISMISS, data)) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the persistent notification component.""" persistent_notifications: MutableMapping[str, MutableMapping] = OrderedDict() hass.data[DOMAIN] = {"notifications": persistent_notifications} diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 7641a75e9c6..ba1f0ced623 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -293,7 +293,7 @@ The following persons point at invalid users: return filtered -async def async_setup(hass: HomeAssistant, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the person component.""" entity_component = EntityComponent(_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py index 9f69c8579a4..f92d087b79d 100644 --- a/homeassistant/components/plum_lightpad/__init__.py +++ b/homeassistant/components/plum_lightpad/__init__.py @@ -10,6 +10,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTAN from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN from .utils import load_plum @@ -34,7 +35,7 @@ CONFIG_SCHEMA = vol.Schema( PLATFORMS = ["light"] -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Plum Lightpad Platform initialization.""" if DOMAIN not in config: return True diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py index 9c650363aad..089e028afd1 100644 --- a/homeassistant/components/proxmoxve/__init__.py +++ b/homeassistant/components/proxmoxve/__init__.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -86,7 +87,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the platform.""" hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index 3e98274c696..e628dfb9813 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -13,6 +13,7 @@ from homeassistant.helpers.entity_registry import ( async_get, async_migrate_entries, ) +from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_POWER, @@ -41,7 +42,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the electricity price sensor from configuration.yaml.""" for conf in config.get(DOMAIN, []): hass.async_create_task( diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index 8b9390bb1c9..42c342a2c84 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -31,6 +31,7 @@ from homeassistant.helpers.entity_component import ( EntityComponent, ) from homeassistant.helpers.reload import async_reload_integration_platforms +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import COORDINATOR, DOMAIN, PLATFORM_IDX, REST, REST_DATA, REST_IDX @@ -43,7 +44,7 @@ PLATFORMS = ["binary_sensor", "notify", "sensor", "switch"] COORDINATOR_AWARE_PLATFORMS = [SENSOR_DOMAIN, BINARY_SENSOR_DOMAIN] -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the rest platforms.""" component = EntityComponent(_LOGGER, DOMAIN, hass) _async_setup_shared_data(hass) diff --git a/homeassistant/components/safe_mode/__init__.py b/homeassistant/components/safe_mode/__init__.py index 94bd95aabe0..162dd204c54 100644 --- a/homeassistant/components/safe_mode/__init__.py +++ b/homeassistant/components/safe_mode/__init__.py @@ -1,11 +1,12 @@ """The Safe Mode integration.""" from homeassistant.components import persistent_notification from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType DOMAIN = "safe_mode" -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Safe Mode component.""" persistent_notification.async_create( hass, diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index 223ca9262ee..2ec087d1e61 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -31,7 +32,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["switch", "sensor", "binary_sensor", "climate"] -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Screenlogic component.""" domain_data = hass.data[DOMAIN] = {} domain_data[DISCOVERED_GATEWAYS] = await async_discover_gateways_by_unique_id(hass) diff --git a/homeassistant/components/search/__init__.py b/homeassistant/components/search/__init__.py index fc13b8ca098..5472ac421c3 100644 --- a/homeassistant/components/search/__init__.py +++ b/homeassistant/components/search/__init__.py @@ -11,12 +11,13 @@ from homeassistant.components.homeassistant import scene from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.entity import entity_sources as get_entity_sources +from homeassistant.helpers.typing import ConfigType DOMAIN = "search" _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Search component.""" websocket_api.async_register_command(hass, websocket_search_related) return True diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py index 1037d399e64..94c5bbcdcac 100644 --- a/homeassistant/components/smappee/__init__.py +++ b/homeassistant/components/smappee/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv +from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle from . import api, config_flow @@ -37,7 +38,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Smappee component.""" hass.data[DOMAIN] = {} diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py index ec4d2c9cad6..06d4de36b3c 100644 --- a/homeassistant/components/smarthab/__init__.py +++ b/homeassistant/components/smarthab/__init__.py @@ -9,6 +9,7 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType DOMAIN = "smarthab" DATA_HUB = "hub" @@ -32,7 +33,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass, config) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the SmartHab platform.""" hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index bc64b173f20..fef2917fb8d 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -57,7 +57,7 @@ from .smartapp import ( _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Initialize the SmartThings platform.""" await setup_smartapp_endpoint(hass) return True diff --git a/homeassistant/components/songpal/__init__.py b/homeassistant/components/songpal/__init__.py index b542591b294..2053d2857c2 100644 --- a/homeassistant/components/songpal/__init__.py +++ b/homeassistant/components/songpal/__init__.py @@ -1,5 +1,4 @@ """The songpal component.""" -from collections import OrderedDict import voluptuous as vol @@ -7,6 +6,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType from .const import CONF_ENDPOINT, DOMAIN @@ -22,7 +22,7 @@ CONFIG_SCHEMA = vol.Schema( PLATFORMS = ["media_player"] -async def async_setup(hass: HomeAssistant, config: OrderedDict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up songpal environment.""" conf = config.get(DOMAIN) if conf is None: diff --git a/homeassistant/components/stt/__init__.py b/homeassistant/components/stt/__init__.py index 694ddeff998..3b5efbcba9c 100644 --- a/homeassistant/components/stt/__init__.py +++ b/homeassistant/components/stt/__init__.py @@ -17,6 +17,7 @@ import attr from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_per_platform, discovery +from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_prepare_setup_platform from .const import ( @@ -34,7 +35,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up STT.""" providers = {} diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index e9a2c5b73a1..87a3260fc40 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -16,6 +16,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_FLAP_ID, @@ -62,7 +63,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Sure Petcare integration.""" conf = config[DOMAIN] hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index 6c13067cd7f..6a23f1bb453 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -16,6 +16,7 @@ from homeassistant.helpers import ( update_coordinator, ) from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_DEVICE_PASSWORD, @@ -49,7 +50,7 @@ CCONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the switcher component.""" hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py index c8200e0e10a..651961c72ac 100644 --- a/homeassistant/components/system_health/__init__.py +++ b/homeassistant/components/system_health/__init__.py @@ -43,7 +43,7 @@ def async_register_info( SystemHealthRegistration(hass, domain).async_register_info(info_callback) -async def async_setup(hass: HomeAssistant, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the System Health component.""" hass.components.websocket_api.async_register_command(handle_info) hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index e2c90098314..2c113b63727 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -56,7 +56,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Tradfri component.""" conf = config.get(DOMAIN) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index c21c1d24f0c..80a7753ec8c 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -55,7 +55,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up UPnP component.""" LOGGER.debug("async_setup, config: %s", config) conf_default = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index feac63f694b..9a153841718 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -25,6 +25,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType from homeassistant.util import convert, slugify from homeassistant.util.dt import utc_from_timestamp @@ -63,7 +64,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, base_config: dict) -> bool: +async def async_setup(hass: HomeAssistant, base_config: ConfigType) -> bool: """Set up for Vera controllers.""" hass.data[DOMAIN] = {} diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index 6e651cdbcf3..d54d79532ca 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -28,6 +28,7 @@ from homeassistant.helpers import ( config_entry_oauth2_flow, config_validation as cv, ) +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import api, config_flow @@ -50,7 +51,7 @@ CONFIG_SCHEMA = vol.Schema( PLATFORMS = ["media_player", "remote", "binary_sensor", "sensor"] -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the xbox component.""" hass.data[DOMAIN] = {} diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 2a4ba4eac55..c9e654bca9a 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -24,6 +24,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -152,7 +153,7 @@ UPDATE_REQUEST_PROPERTIES = [ PLATFORMS = ["binary_sensor", "light"] -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Yeelight bulbs.""" conf = config.get(DOMAIN, {}) hass.data[DOMAIN] = { diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index e7132f56b55..a85236b6a07 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -28,6 +28,7 @@ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.network import NoURLAvailableError, get_url +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_homekit, async_get_zeroconf, bind_hass from .models import HaAsyncServiceBrowser, HaAsyncZeroconf, HaZeroconf @@ -137,7 +138,7 @@ def _async_use_default_interface(adapters: list[Adapter]) -> bool: return True -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Zeroconf and make Home Assistant discoverable.""" zc_args: dict = {} diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 8ab0e9b2703..d4474d793ab 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -30,6 +30,7 @@ from homeassistant.helpers import ( service, storage, ) +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util.location import distance @@ -176,7 +177,7 @@ class ZoneStorageCollection(collection.StorageCollection): return {**data, **update_data} -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up configured zones as well as Home Assistant zone if necessary.""" component = entity_component.EntityComponent(_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 6320efddb60..c8f2bd19776 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -28,6 +28,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.typing import ConfigType from .addon import AddonError, AddonManager, AddonState, get_addon_manager from .api import async_register_api @@ -79,7 +80,7 @@ DATA_CONNECT_FAILED_LOGGED = "connect_failed_logged" DATA_INVALID_SERVER_VERSION_LOGGED = "invalid_server_version_logged" -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Z-Wave JS component.""" hass.data[DOMAIN] = {} return True diff --git a/homeassistant/config.py b/homeassistant/config.py index cd159dfc8ce..754420dbcce 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -925,7 +925,7 @@ async def async_process_component_config( # noqa: C901 @callback -def config_without_domain(config: dict, domain: str) -> dict: +def config_without_domain(config: ConfigType, domain: str) -> ConfigType: """Return a config with all configuration for a domain removed.""" filter_keys = extract_domain_configs(config, domain) return {key: value for key, value in config.items() if key not in filter_keys} diff --git a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py index f597ef609ea..8b1bdc93749 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py @@ -1,8 +1,6 @@ """The NEW_NAME integration.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.config_entries import ConfigEntry @@ -13,6 +11,7 @@ from homeassistant.helpers import ( config_entry_oauth2_flow, config_validation as cv, ) +from homeassistant.helpers.typing import ConfigType from . import api, config_flow from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN @@ -34,7 +33,7 @@ CONFIG_SCHEMA = vol.Schema( PLATFORMS = ["light"] -async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the NEW_NAME component.""" hass.data[DOMAIN] = {} diff --git a/script/scaffold/templates/integration/integration/__init__.py b/script/scaffold/templates/integration/integration/__init__.py index c1f34d5f5b1..e30cd400bf2 100644 --- a/script/scaffold/templates/integration/integration/__init__.py +++ b/script/scaffold/templates/integration/integration/__init__.py @@ -1,17 +1,16 @@ """The NEW_NAME integration.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN CONFIG_SCHEMA = vol.Schema({vol.Optional(DOMAIN): {}}, extra=vol.ALLOW_EXTRA) -async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the NEW_NAME integration.""" return True From 3e235f6e70333524208799c5a4052a38743230f3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 13:36:35 +0200 Subject: [PATCH 310/355] Remove `last_reset` attribute and set state class to `total_increasing` for Ovo cost and energy sensors (#54807) --- homeassistant/components/ovo_energy/sensor.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index 91290238dce..e1130ca36a5 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -6,12 +6,11 @@ from datetime import timedelta from ovoenergy import OVODailyUsage from ovoenergy.ovoenergy import OVOEnergy -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_TOTAL_INCREASING, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_ENERGY, DEVICE_CLASS_MONETARY from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from homeassistant.util.dt import utc_from_timestamp from . import OVOEnergyDeviceEntity from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN @@ -61,8 +60,7 @@ async def async_setup_entry( class OVOEnergySensor(OVOEnergyDeviceEntity, SensorEntity): """Defines a OVO Energy sensor.""" - _attr_last_reset = utc_from_timestamp(0) - _attr_state_class = "measurement" + _attr_state_class = STATE_CLASS_TOTAL_INCREASING def __init__( self, From 7812b50572dd54f58cd6e4a196e8d614300adab1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 13:37:43 +0200 Subject: [PATCH 311/355] Remove `last_reset` attribute and set state class to `total_increasing` for powerwall energy sensors (#54808) --- homeassistant/components/powerwall/sensor.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index 0ffa333181d..940dcad8647 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -3,7 +3,11 @@ import logging from tesla_powerwall import MeterType -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, +) from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_ENERGY, @@ -12,7 +16,6 @@ from homeassistant.const import ( PERCENTAGE, POWER_KILO_WATT, ) -import homeassistant.util.dt as dt_util from .const import ( ATTR_FREQUENCY, @@ -151,10 +154,9 @@ class PowerWallEnergySensor(PowerWallEntity, SensorEntity): class PowerWallEnergyDirectionSensor(PowerWallEntity, SensorEntity): """Representation of an Powerwall Direction Energy sensor.""" - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = STATE_CLASS_TOTAL_INCREASING _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR _attr_device_class = DEVICE_CLASS_ENERGY - _attr_last_reset = dt_util.utc_from_timestamp(0) def __init__( self, From 3c5ba1fcc3476cbdf13f13536039e669053d1b25 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 13:41:57 +0200 Subject: [PATCH 312/355] Remove `last_reset` attribute and set state class to `total_increasing` for PVOutput energy sensors (#54809) --- homeassistant/components/pvoutput/sensor.py | 42 ++------------------- 1 file changed, 4 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 722aea8e868..8126e00d8e5 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -2,9 +2,8 @@ from __future__ import annotations from collections import namedtuple -from datetime import datetime, timedelta +from datetime import timedelta import logging -from typing import cast import voluptuous as vol @@ -12,7 +11,7 @@ from homeassistant.components.rest.data import RestData from homeassistant.components.sensor import ( DEVICE_CLASS_ENERGY, PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.const import ( @@ -26,8 +25,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) _ENDPOINT = "http://pvoutput.org/service/r2/getstatus.jsp" @@ -74,15 +71,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([PvoutputSensor(rest, name)]) -class PvoutputSensor(SensorEntity, RestoreEntity): +class PvoutputSensor(SensorEntity): """Representation of a PVOutput sensor.""" - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = STATE_CLASS_TOTAL_INCREASING _attr_device_class = DEVICE_CLASS_ENERGY _attr_native_unit_of_measurement = ENERGY_WATT_HOUR - _old_state: int | None = None - def __init__(self, rest, name): """Initialize a PVOutput sensor.""" self.rest = rest @@ -129,37 +124,8 @@ class PvoutputSensor(SensorEntity, RestoreEntity): await self.rest.async_update() self._async_update_from_rest_data() - new_state: int | None = None - state = cast("str | None", self.state) - if state is not None: - new_state = int(state) - - did_reset = False - if new_state is None: - did_reset = False - elif self._old_state is None: - did_reset = True - elif new_state == 0: - did_reset = self._old_state != 0 - elif new_state < self._old_state: - did_reset = True - - if did_reset: - self._attr_last_reset = dt_util.utcnow() - - if new_state is not None: - self._old_state = new_state - async def async_added_to_hass(self): """Ensure the data from the initial update is reflected in the state.""" - last_state = await self.async_get_last_state() - if last_state is not None: - if "last_reset" in last_state.attributes: - self._attr_last_reset = dt_util.as_utc( - datetime.fromisoformat(last_state.attributes["last_reset"]) - ) - self._old_state = int(last_state.state) - self._async_update_from_rest_data() @callback From d9bfb8fc58f54c4460a853fac46b70e970dd9819 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 13:44:08 +0200 Subject: [PATCH 313/355] Remove `last_reset` attribute and set state class to `total_increasing` for rainforest energy sensors (#54810) --- homeassistant/components/rainforest_eagle/sensor.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/rainforest_eagle/sensor.py b/homeassistant/components/rainforest_eagle/sensor.py index 64eb243c15d..6e42d2a13a2 100644 --- a/homeassistant/components/rainforest_eagle/sensor.py +++ b/homeassistant/components/rainforest_eagle/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime, timedelta +from datetime import timedelta import logging from eagle200_reader import EagleReader @@ -14,6 +14,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_ENERGY, PLATFORM_SCHEMA, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.const import ( @@ -22,7 +23,7 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, ) import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle, dt +from homeassistant.util import Throttle CONF_CLOUD_ID = "cloud_id" CONF_INSTALL_CODE = "install_code" @@ -41,7 +42,6 @@ class SensorType: unit_of_measurement: str device_class: str | None = None state_class: str | None = None - last_reset: datetime | None = None SENSORS = { @@ -54,15 +54,13 @@ SENSORS = { name="Eagle-200 Total Meter Energy Delivered", unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset=dt.utc_from_timestamp(0), + state_class=STATE_CLASS_TOTAL_INCREASING, ), "summation_received": SensorType( name="Eagle-200 Total Meter Energy Received", unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset=dt.utc_from_timestamp(0), + state_class=STATE_CLASS_TOTAL_INCREASING, ), "summation_total": SensorType( name="Eagle-200 Net Meter Energy (Delivered minus Received)", @@ -134,7 +132,6 @@ class EagleSensor(SensorEntity): self._attr_native_unit_of_measurement = sensor_info.unit_of_measurement self._attr_device_class = sensor_info.device_class self._attr_state_class = sensor_info.state_class - self._attr_last_reset = sensor_info.last_reset def update(self): """Get the energy information from the Rainforest Eagle.""" From 0b7b4152f1abb95a6c43cfa0a8ff517d6f964312 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 13:55:21 +0200 Subject: [PATCH 314/355] Remove last_reset attribute from devolo energy sensors (#54803) --- .../components/devolo_home_control/sensor.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index 5c8bed7818b..61c3e9a5c19 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -12,7 +12,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -162,10 +162,7 @@ class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity): ) if consumption == "total": - self._attr_state_class = STATE_CLASS_MEASUREMENT - self._attr_last_reset = device_instance.consumption_property[ - element_uid - ].total_since + self._attr_state_class = STATE_CLASS_TOTAL_INCREASING self._value = getattr( device_instance.consumption_property[element_uid], consumption @@ -180,15 +177,11 @@ class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity): def _sync(self, message: tuple) -> None: """Update the consumption sensor state.""" - if message[0] == self._attr_unique_id and message[2] != "total_since": + if message[0] == self._attr_unique_id: self._value = getattr( self._device_instance.consumption_property[self._attr_unique_id], self._sensor_type, ) - elif message[0] == self._attr_unique_id and message[2] == "total_since": - self._attr_last_reset = self._device_instance.consumption_property[ - self._attr_unique_id - ].total_since else: self._generic_message(message) self.schedule_update_ha_state() From 60f8e24bde91d50cdb588fcab66c2a6c5dccd492 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 13:58:08 +0200 Subject: [PATCH 315/355] Remove last_reset attribute from sma energy sensors (#54814) --- homeassistant/components/sma/sensor.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 36084c53bb3..8808272ad75 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -32,7 +32,6 @@ from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -from homeassistant.util import dt as dt_util from .const import ( CONF_CUSTOM, @@ -165,9 +164,8 @@ class SMAsensor(CoordinatorEntity, SensorEntity): self._device_info = device_info if self.unit_of_measurement == ENERGY_KILO_WATT_HOUR: - self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_state_class = STATE_CLASS_TOTAL_INCREASING self._attr_device_class = DEVICE_CLASS_ENERGY - self._attr_last_reset = dt_util.utc_from_timestamp(0) # Set sensor enabled to False. # Will be enabled by async_added_to_hass if actually used. From 0329d0f2465084cd95c888032fd405cc56fe7643 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 14:18:51 +0200 Subject: [PATCH 316/355] Remove last_reset attribute and set state class to total_increasing for tibber energy sensors (#54799) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove last_reset attribute from tibber energy sensors * Remove reset_type, fix merge * Update homeassistant/components/tibber/sensor.py Co-authored-by: Franck Nijhof Co-authored-by: Daniel Hjelseth Høyer Co-authored-by: Franck Nijhof --- homeassistant/components/tibber/sensor.py | 108 ++++++---------------- 1 file changed, 30 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 080da3fca13..d376bf0a7d5 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -2,9 +2,7 @@ from __future__ import annotations import asyncio -from dataclasses import dataclass from datetime import timedelta -from enum import Enum import logging from random import randrange @@ -19,6 +17,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, ) @@ -49,166 +48,143 @@ PARALLEL_UPDATES = 0 SIGNAL_UPDATE_ENTITY = "tibber_rt_update_{}" -class ResetType(Enum): - """Data reset type.""" - - HOURLY = "hourly" - DAILY = "daily" - NEVER = "never" - - -@dataclass -class TibberSensorEntityDescription(SensorEntityDescription): - """Describes Tibber sensor entity.""" - - reset_type: ResetType | None = None - - -RT_SENSORS: tuple[TibberSensorEntityDescription, ...] = ( - TibberSensorEntityDescription( +RT_SENSORS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( key="averagePower", name="average power", device_class=DEVICE_CLASS_POWER, native_unit_of_measurement=POWER_WATT, ), - TibberSensorEntityDescription( + SensorEntityDescription( key="power", name="power", device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), - TibberSensorEntityDescription( + SensorEntityDescription( key="powerProduction", name="power production", device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), - TibberSensorEntityDescription( + SensorEntityDescription( key="minPower", name="min power", device_class=DEVICE_CLASS_POWER, native_unit_of_measurement=POWER_WATT, ), - TibberSensorEntityDescription( + SensorEntityDescription( key="maxPower", name="max power", device_class=DEVICE_CLASS_POWER, native_unit_of_measurement=POWER_WATT, ), - TibberSensorEntityDescription( + SensorEntityDescription( key="accumulatedConsumption", name="accumulated consumption", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_MEASUREMENT, - reset_type=ResetType.DAILY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), - TibberSensorEntityDescription( + SensorEntityDescription( key="accumulatedConsumptionLastHour", name="accumulated consumption current hour", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_MEASUREMENT, - reset_type=ResetType.HOURLY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), - TibberSensorEntityDescription( + SensorEntityDescription( key="accumulatedProduction", name="accumulated production", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_MEASUREMENT, - reset_type=ResetType.DAILY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), - TibberSensorEntityDescription( + SensorEntityDescription( key="accumulatedProductionLastHour", name="accumulated production current hour", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_MEASUREMENT, - reset_type=ResetType.HOURLY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), - TibberSensorEntityDescription( + SensorEntityDescription( key="lastMeterConsumption", name="last meter consumption", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_MEASUREMENT, - reset_type=ResetType.NEVER, + state_class=STATE_CLASS_TOTAL_INCREASING, ), - TibberSensorEntityDescription( + SensorEntityDescription( key="lastMeterProduction", name="last meter production", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_MEASUREMENT, - reset_type=ResetType.NEVER, + state_class=STATE_CLASS_TOTAL_INCREASING, ), - TibberSensorEntityDescription( + SensorEntityDescription( key="voltagePhase1", name="voltage phase1", device_class=DEVICE_CLASS_VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), - TibberSensorEntityDescription( + SensorEntityDescription( key="voltagePhase2", name="voltage phase2", device_class=DEVICE_CLASS_VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), - TibberSensorEntityDescription( + SensorEntityDescription( key="voltagePhase3", name="voltage phase3", device_class=DEVICE_CLASS_VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=STATE_CLASS_MEASUREMENT, ), - TibberSensorEntityDescription( + SensorEntityDescription( key="currentL1", name="current L1", device_class=DEVICE_CLASS_CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), - TibberSensorEntityDescription( + SensorEntityDescription( key="currentL2", name="current L2", device_class=DEVICE_CLASS_CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), - TibberSensorEntityDescription( + SensorEntityDescription( key="currentL3", name="current L3", device_class=DEVICE_CLASS_CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=STATE_CLASS_MEASUREMENT, ), - TibberSensorEntityDescription( + SensorEntityDescription( key="signalStrength", name="signal strength", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, state_class=STATE_CLASS_MEASUREMENT, ), - TibberSensorEntityDescription( + SensorEntityDescription( key="accumulatedReward", name="accumulated reward", device_class=DEVICE_CLASS_MONETARY, state_class=STATE_CLASS_MEASUREMENT, - reset_type=ResetType.DAILY, ), - TibberSensorEntityDescription( + SensorEntityDescription( key="accumulatedCost", name="accumulated cost", device_class=DEVICE_CLASS_MONETARY, state_class=STATE_CLASS_MEASUREMENT, - reset_type=ResetType.DAILY, ), - TibberSensorEntityDescription( + SensorEntityDescription( key="powerFactor", name="power factor", device_class=DEVICE_CLASS_POWER_FACTOR, @@ -376,12 +352,10 @@ class TibberSensorElPrice(TibberSensor): class TibberSensorRT(TibberSensor, update_coordinator.CoordinatorEntity): """Representation of a Tibber sensor for real time consumption.""" - entity_description: TibberSensorEntityDescription - def __init__( self, tibber_home, - description: TibberSensorEntityDescription, + description: SensorEntityDescription, initial_state, coordinator: TibberRtDataCoordinator, ): @@ -397,18 +371,6 @@ class TibberSensorRT(TibberSensor, update_coordinator.CoordinatorEntity): if description.key in ("accumulatedCost", "accumulatedReward"): self._attr_native_unit_of_measurement = tibber_home.currency - if description.reset_type == ResetType.NEVER: - self._attr_last_reset = dt_util.utc_from_timestamp(0) - elif description.reset_type == ResetType.DAILY: - self._attr_last_reset = dt_util.as_utc( - dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) - ) - elif description.reset_type == ResetType.HOURLY: - self._attr_last_reset = dt_util.as_utc( - dt_util.now().replace(minute=0, second=0, microsecond=0) - ) - else: - self._attr_last_reset = None @property def available(self): @@ -422,16 +384,6 @@ class TibberSensorRT(TibberSensor, update_coordinator.CoordinatorEntity): state = live_measurement.get(self.entity_description.key) if state is None: return - timestamp = dt_util.parse_datetime(live_measurement["timestamp"]) - if timestamp is not None and state < self.state: - if self.entity_description.reset_type == ResetType.DAILY: - self._attr_last_reset = dt_util.as_utc( - timestamp.replace(hour=0, minute=0, second=0, microsecond=0) - ) - elif self.entity_description.reset_type == ResetType.HOURLY: - self._attr_last_reset = dt_util.as_utc( - timestamp.replace(minute=0, second=0, microsecond=0) - ) if self.entity_description.key == "powerFactor": state *= 100.0 self._attr_native_value = state From aef8ec968bf604df9f46addbe33adada4733642e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 14:59:22 +0200 Subject: [PATCH 317/355] Remove last_reset attribute from kostal_plenticore energy sensors (#54817) --- .../components/kostal_plenticore/const.py | 26 +++++++------------ .../components/kostal_plenticore/sensor.py | 13 ++-------- 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/const.py b/homeassistant/components/kostal_plenticore/const.py index ede8e10cb25..5cbc1a2af79 100644 --- a/homeassistant/components/kostal_plenticore/const.py +++ b/homeassistant/components/kostal_plenticore/const.py @@ -1,9 +1,9 @@ """Constants for the Kostal Plenticore Solar Inverter integration.""" from homeassistant.components.sensor import ( - ATTR_LAST_RESET, ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -304,8 +304,7 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: LAST_RESET_NEVER, + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, "format_energy", ), @@ -346,8 +345,7 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: LAST_RESET_NEVER, + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, "format_energy", ), @@ -388,8 +386,7 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: LAST_RESET_NEVER, + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, "format_energy", ), @@ -430,8 +427,7 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: LAST_RESET_NEVER, + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, "format_energy", ), @@ -472,8 +468,7 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: LAST_RESET_NEVER, + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, "format_energy", ), @@ -514,8 +509,7 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: LAST_RESET_NEVER, + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, "format_energy", ), @@ -556,8 +550,7 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: LAST_RESET_NEVER, + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, "format_energy", ), @@ -599,8 +592,7 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: LAST_RESET_NEVER, + ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, "format_energy", ), diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py index 57b37e51d11..19ac4db0f90 100644 --- a/homeassistant/components/kostal_plenticore/sensor.py +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -1,15 +1,11 @@ """Platform for Kostal Plenticore sensors.""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import timedelta import logging from typing import Any, Callable -from homeassistant.components.sensor import ( - ATTR_LAST_RESET, - ATTR_STATE_CLASS, - SensorEntity, -) +from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant @@ -193,11 +189,6 @@ class PlenticoreDataSensor(CoordinatorEntity, SensorEntity): """Return if the entity should be enabled when first added to the entity registry.""" return self._sensor_data.get(ATTR_ENABLED_DEFAULT, False) - @property - def last_reset(self) -> datetime | None: - """Return the last_reset time.""" - return self._sensor_data.get(ATTR_LAST_RESET) - @property def native_value(self) -> Any | None: """Return the state of the sensor.""" From 5536e24dec7d1cfd221eeb881425d85c1dd3bdbd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 15:11:10 +0200 Subject: [PATCH 318/355] Remove `last_reset` attribute and set state class to `total_increasing` for zwave_js energy sensors (#54818) --- homeassistant/components/zwave_js/sensor.py | 53 ++------------- tests/components/zwave_js/common.py | 5 -- tests/components/zwave_js/conftest.py | 18 ------ tests/components/zwave_js/test_sensor.py | 71 ++++----------------- 4 files changed, 18 insertions(+), 129 deletions(-) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index deacf3d874a..220184f5669 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -11,13 +11,13 @@ from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.value import ConfigurationValue from homeassistant.components.sensor import ( - ATTR_LAST_RESET, DEVICE_CLASS_BATTERY, DEVICE_CLASS_ENERGY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DOMAIN as SENSOR_DOMAIN, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -36,8 +36,6 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.util import dt from .const import ATTR_METER_TYPE, ATTR_VALUE, DATA_CLIENT, DOMAIN, SERVICE_RESET_METER from .discovery import ZwaveDiscoveryInfo @@ -218,7 +216,7 @@ class ZWaveNumericSensor(ZwaveSensorBase): return str(self.info.primary_value.metadata.unit) -class ZWaveMeterSensor(ZWaveNumericSensor, RestoreEntity): +class ZWaveMeterSensor(ZWaveNumericSensor): """Representation of a Z-Wave Meter CC sensor.""" def __init__( @@ -231,51 +229,10 @@ class ZWaveMeterSensor(ZWaveNumericSensor, RestoreEntity): super().__init__(config_entry, client, info) # Entity class attributes - self._attr_state_class = STATE_CLASS_MEASUREMENT if self.device_class == DEVICE_CLASS_ENERGY: - self._attr_last_reset = dt.utc_from_timestamp(0) - - @callback - def async_update_last_reset( - self, node: ZwaveNode, endpoint: int, meter_type: int | None - ) -> None: - """Update last reset.""" - # If the signal is not for this node or is for a different endpoint, - # or a meter type was specified and doesn't match this entity's meter type: - if ( - self.info.node != node - or self.info.primary_value.endpoint != endpoint - or meter_type is not None - and self.info.primary_value.metadata.cc_specific.get("meterType") - != meter_type - ): - return - - self._attr_last_reset = dt.utcnow() - self.async_write_ha_state() - - async def async_added_to_hass(self) -> None: - """Call when entity is added.""" - await super().async_added_to_hass() - - # If the meter is not an accumulating meter type, do not reset. - if self.device_class != DEVICE_CLASS_ENERGY: - return - - # Restore the last reset time from stored state - restored_state = await self.async_get_last_state() - if restored_state and ATTR_LAST_RESET in restored_state.attributes: - self._attr_last_reset = dt.parse_datetime( - restored_state.attributes[ATTR_LAST_RESET] - ) - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{DOMAIN}_{SERVICE_RESET_METER}", - self.async_update_last_reset, - ) - ) + self._attr_state_class = STATE_CLASS_TOTAL_INCREASING + else: + self._attr_state_class = STATE_CLASS_MEASUREMENT async def async_reset_meter( self, meter_type: int | None = None, value: int | None = None diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index 0c6b19698a9..2590149c462 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -1,6 +1,4 @@ """Provide common test tools for Z-Wave JS.""" -from datetime import datetime, timezone - AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature" HUMIDITY_SENSOR = "sensor.multisensor_6_humidity" POWER_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" @@ -35,6 +33,3 @@ ID_LOCK_CONFIG_PARAMETER_SENSOR = ( ZEN_31_ENTITY = "light.kitchen_under_cabinet_lights" METER_ENERGY_SENSOR = "sensor.smart_switch_6_electric_consumed_kwh" METER_VOLTAGE_SENSOR = "sensor.smart_switch_6_electric_consumed_v" - -DATETIME_ZERO = datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc) -DATETIME_LAST_RESET = datetime(2020, 1, 1, 0, 0, 0, tzinfo=timezone.utc) diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 8165dac33a7..900a7937539 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -11,11 +11,6 @@ from zwave_js_server.model.driver import Driver from zwave_js_server.model.node import Node from zwave_js_server.version import VersionInfo -from homeassistant.components.sensor import ATTR_LAST_RESET -from homeassistant.core import State - -from .common import DATETIME_LAST_RESET - from tests.common import MockConfigEntry, load_fixture # Add-on fixtures @@ -858,16 +853,3 @@ def lock_popp_electric_strike_lock_control_fixture( def firmware_file_fixture(): """Return mock firmware file stream.""" return io.BytesIO(bytes(10)) - - -@pytest.fixture(name="restore_last_reset") -def restore_last_reset_fixture(): - """Return mock restore last reset.""" - state = State( - "sensor.test", "test", {ATTR_LAST_RESET: DATETIME_LAST_RESET.isoformat()} - ) - with patch( - "homeassistant.components.zwave_js.sensor.ZWaveMeterSensor.async_get_last_state", - return_value=state, - ): - yield state diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index 268d8ee1380..6d64f6f92dd 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -1,9 +1,10 @@ """Test the Z-Wave JS sensor platform.""" -from unittest.mock import patch - from zwave_js_server.event import Event -from homeassistant.components.sensor import ATTR_LAST_RESET, STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, +) from homeassistant.components.zwave_js.const import ( ATTR_METER_TYPE, ATTR_VALUE, @@ -29,14 +30,11 @@ from homeassistant.helpers import entity_registry as er from .common import ( AIR_TEMPERATURE_SENSOR, CURRENT_SENSOR, - DATETIME_LAST_RESET, - DATETIME_ZERO, ENERGY_SENSOR, HUMIDITY_SENSOR, ID_LOCK_CONFIG_PARAMETER_SENSOR, INDICATOR_SENSOR, METER_ENERGY_SENSOR, - METER_VOLTAGE_SENSOR, NOTIFICATION_MOTION_SENSOR, POWER_SENSOR, VOLTAGE_SENSOR, @@ -76,7 +74,7 @@ async def test_energy_sensors(hass, hank_binary_switch, integration): assert state.state == "0.16" assert state.attributes["unit_of_measurement"] == ENERGY_KILO_WATT_HOUR assert state.attributes["device_class"] == DEVICE_CLASS_ENERGY - assert state.attributes["state_class"] == STATE_CLASS_MEASUREMENT + assert state.attributes["state_class"] == STATE_CLASS_TOTAL_INCREASING state = hass.states.get(VOLTAGE_SENSOR) @@ -192,31 +190,14 @@ async def test_reset_meter( client.async_send_command.return_value = {} client.async_send_command_no_wait.return_value = {} - # Validate that non accumulating meter does not have a last reset attribute - - assert ATTR_LAST_RESET not in hass.states.get(METER_VOLTAGE_SENSOR).attributes - - # Validate that the sensor last reset is starting from nothing - assert ( - hass.states.get(METER_ENERGY_SENSOR).attributes[ATTR_LAST_RESET] - == DATETIME_ZERO.isoformat() - ) - - # Test successful meter reset call, patching utcnow so we can make sure the last - # reset gets updated - with patch("homeassistant.util.dt.utcnow", return_value=DATETIME_LAST_RESET): - await hass.services.async_call( - DOMAIN, - SERVICE_RESET_METER, - { - ATTR_ENTITY_ID: METER_ENERGY_SENSOR, - }, - blocking=True, - ) - - assert ( - hass.states.get(METER_ENERGY_SENSOR).attributes[ATTR_LAST_RESET] - == DATETIME_LAST_RESET.isoformat() + # Test successful meter reset call + await hass.services.async_call( + DOMAIN, + SERVICE_RESET_METER, + { + ATTR_ENTITY_ID: METER_ENERGY_SENSOR, + }, + blocking=True, ) assert len(client.async_send_command_no_wait.call_args_list) == 1 @@ -226,10 +207,6 @@ async def test_reset_meter( assert args["endpoint"] == 0 assert args["args"] == [] - # Validate that non accumulating meter does not have a last reset attribute - - assert ATTR_LAST_RESET not in hass.states.get(METER_VOLTAGE_SENSOR).attributes - client.async_send_command_no_wait.reset_mock() # Test successful meter reset call with options @@ -251,26 +228,4 @@ async def test_reset_meter( assert args["endpoint"] == 0 assert args["args"] == [{"type": 1, "targetValue": 2}] - # Validate that non accumulating meter does not have a last reset attribute - - assert ATTR_LAST_RESET not in hass.states.get(METER_VOLTAGE_SENSOR).attributes - client.async_send_command_no_wait.reset_mock() - - -async def test_restore_last_reset( - hass, - client, - aeon_smart_switch_6, - restore_last_reset, - integration, -): - """Test restoring last_reset on setup.""" - assert ( - hass.states.get(METER_ENERGY_SENSOR).attributes[ATTR_LAST_RESET] - == DATETIME_LAST_RESET.isoformat() - ) - - # Validate that non accumulating meter does not have a last reset attribute - - assert ATTR_LAST_RESET not in hass.states.get(METER_VOLTAGE_SENSOR).attributes From 20b7125620d1ba6daf53756daea174c67636bec6 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 18 Aug 2021 15:34:50 +0200 Subject: [PATCH 319/355] Activate mypy for Panasonic_viera (#54547) --- homeassistant/components/panasonic_viera/__init__.py | 2 +- homeassistant/components/panasonic_viera/config_flow.py | 2 +- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index e187b7c18a5..ab63b535e80 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -1,7 +1,7 @@ """The Panasonic Viera integration.""" from functools import partial import logging -from urllib.request import HTTPError, URLError +from urllib.error import HTTPError, URLError from panasonic_viera import EncryptionRequired, Keys, RemoteControl, SOAPError import voluptuous as vol diff --git a/homeassistant/components/panasonic_viera/config_flow.py b/homeassistant/components/panasonic_viera/config_flow.py index 42400e7348c..d1c6461de21 100644 --- a/homeassistant/components/panasonic_viera/config_flow.py +++ b/homeassistant/components/panasonic_viera/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Panasonic Viera TV integration.""" from functools import partial import logging -from urllib.request import URLError +from urllib.error import URLError from panasonic_viera import TV_TYPE_ENCRYPTED, RemoteControl, SOAPError import voluptuous as vol diff --git a/mypy.ini b/mypy.ini index 3108f73a49e..cb6adf5d62e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1550,9 +1550,6 @@ ignore_errors = true [mypy-homeassistant.components.ozw.*] ignore_errors = true -[mypy-homeassistant.components.panasonic_viera.*] -ignore_errors = true - [mypy-homeassistant.components.philips_js.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 6a863355afc..73081ddfc53 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -109,7 +109,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.onvif.*", "homeassistant.components.ovo_energy.*", "homeassistant.components.ozw.*", - "homeassistant.components.panasonic_viera.*", "homeassistant.components.philips_js.*", "homeassistant.components.ping.*", "homeassistant.components.pioneer.*", From 07c0fc9ebad599087d47c8916c916850c8ed2e9f Mon Sep 17 00:00:00 2001 From: SmaginPV Date: Wed, 18 Aug 2021 16:53:17 +0300 Subject: [PATCH 320/355] Remove deprecated Xiaomi Miio fan speeds (#54182) --- homeassistant/components/xiaomi_miio/fan.py | 111 -------------------- 1 file changed, 111 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 35c3765d985..87e8fa0ca2a 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -420,20 +420,12 @@ class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): self._supported_features = 0 self._speed_count = 100 self._preset_modes = [] - # the speed_list attribute is deprecated, support will end with release 2021.7 - self._speed_list = [] @property def supported_features(self): """Flag supported features.""" return self._supported_features - # the speed_list attribute is deprecated, support will end with release 2021.7 - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return self._speed_list - @property def speed_count(self): """Return the number of speeds of the fan supported.""" @@ -516,9 +508,6 @@ class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): "Turning the miio device on failed.", self._device.on ) - # Remove the async_set_speed call is async_set_percentage and async_set_preset_modes have been implemented - if speed: - await self.async_set_speed(speed) # If operation mode was set the device must not be turned on. if percentage: await self.async_set_percentage(percentage) @@ -610,61 +599,39 @@ class XiaomiAirPurifier(XiaomiGenericDevice): if self._model == MODEL_AIRPURIFIER_PRO: self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO - # SUPPORT_SET_SPEED was disabled - # the device supports preset_modes only self._preset_modes = PRESET_MODES_AIRPURIFIER_PRO self._supported_features = SUPPORT_PRESET_MODE self._speed_count = 1 - # the speed_list attribute is deprecated, support will end with release 2021.7 - self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO elif self._model == MODEL_AIRPURIFIER_PRO_V7: self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO_V7 self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7 - # SUPPORT_SET_SPEED was disabled - # the device supports preset_modes only self._preset_modes = PRESET_MODES_AIRPURIFIER_PRO_V7 self._supported_features = SUPPORT_PRESET_MODE self._speed_count = 1 - # the speed_list attribute is deprecated, support will end with release 2021.7 - self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO_V7 elif self._model in [MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_2H]: self._device_features = FEATURE_FLAGS_AIRPURIFIER_2S self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_2S - # SUPPORT_SET_SPEED was disabled - # the device supports preset_modes only self._preset_modes = PRESET_MODES_AIRPURIFIER_2S self._supported_features = SUPPORT_PRESET_MODE self._speed_count = 1 - # the speed_list attribute is deprecated, support will end with release 2021.7 - self._speed_list = OPERATION_MODES_AIRPURIFIER_2S elif self._model in MODELS_PURIFIER_MIOT: self._device_features = FEATURE_FLAGS_AIRPURIFIER_3 self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_3 - # SUPPORT_SET_SPEED was disabled - # the device supports preset_modes only self._preset_modes = PRESET_MODES_AIRPURIFIER_3 self._supported_features = SUPPORT_SET_SPEED | SUPPORT_PRESET_MODE self._speed_count = 3 - # the speed_list attribute is deprecated, support will end with release 2021.7 - self._speed_list = OPERATION_MODES_AIRPURIFIER_3 elif self._model == MODEL_AIRPURIFIER_V3: self._device_features = FEATURE_FLAGS_AIRPURIFIER_V3 self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 - # SUPPORT_SET_SPEED was disabled - # the device supports preset_modes only self._preset_modes = PRESET_MODES_AIRPURIFIER_V3 self._supported_features = SUPPORT_PRESET_MODE self._speed_count = 1 - # the speed_list attribute is deprecated, support will end with release 2021.7 - self._speed_list = OPERATION_MODES_AIRPURIFIER_V3 else: self._device_features = FEATURE_FLAGS_AIRPURIFIER self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER self._preset_modes = PRESET_MODES_AIRPURIFIER self._supported_features = SUPPORT_PRESET_MODE self._speed_count = 1 - # the speed_list attribute is deprecated, support will end with release 2021.7 - self._speed_list = [] self._state_attrs.update( {attribute: None for attribute in self._available_attributes} @@ -693,15 +660,6 @@ class XiaomiAirPurifier(XiaomiGenericDevice): return None - # the speed attribute is deprecated, support will end with release 2021.7 - @property - def speed(self): - """Return the current speed.""" - if self._state: - return AirpurifierOperationMode(self._state_attrs[ATTR_MODE]).name - - return None - async def async_set_percentage(self, percentage: int) -> None: """Set the percentage of the fan. @@ -731,21 +689,6 @@ class XiaomiAirPurifier(XiaomiGenericDevice): self.PRESET_MODE_MAPPING[preset_mode], ) - # the async_set_speed function is deprecated, support will end with release 2021.7 - # it is added here only for compatibility with legacy speeds - async def async_set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - if self.supported_features & SUPPORT_SET_SPEED == 0: - return - - _LOGGER.debug("Setting the operation mode to: %s", speed) - - await self._try_command( - "Setting operation mode of the miio device failed.", - self._device.set_mode, - AirpurifierOperationMode[speed.title()], - ) - async def async_set_led_on(self): """Turn the led on.""" if self._device_features & FEATURE_SET_LED == 0: @@ -892,15 +835,6 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): return None - # the speed attribute is deprecated, support will end with release 2021.7 - @property - def speed(self): - """Return the current speed.""" - if self._state: - return AirpurifierMiotOperationMode(self._mode).name - - return None - async def async_set_percentage(self, percentage: int) -> None: """Set the percentage of the fan. @@ -933,23 +867,6 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): self._mode = self.PRESET_MODE_MAPPING[preset_mode].value self.async_write_ha_state() - # the async_set_speed function is deprecated, support will end with release 2021.7 - # it is added here only for compatibility with legacy speeds - async def async_set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - if self.supported_features & SUPPORT_SET_SPEED == 0: - return - - _LOGGER.debug("Setting the operation mode to: %s", speed) - - if await self._try_command( - "Setting operation mode of the miio device failed.", - self._device.set_mode, - AirpurifierMiotOperationMode[speed.title()], - ): - self._mode = AirpurifierMiotOperationMode[speed.title()].value - self.async_write_ha_state() - class XiaomiAirFresh(XiaomiGenericDevice): """Representation of a Xiaomi Air Fresh.""" @@ -974,8 +891,6 @@ class XiaomiAirFresh(XiaomiGenericDevice): self._device_features = FEATURE_FLAGS_AIRFRESH self._available_attributes = AVAILABLE_ATTRIBUTES_AIRFRESH - # the speed_list attribute is deprecated, support will end with release 2021.7 - self._speed_list = OPERATION_MODES_AIRFRESH self._speed_count = 4 self._preset_modes = PRESET_MODES_AIRFRESH self._supported_features = SUPPORT_SET_SPEED | SUPPORT_PRESET_MODE @@ -1005,15 +920,6 @@ class XiaomiAirFresh(XiaomiGenericDevice): return None - # the speed attribute is deprecated, support will end with release 2021.7 - @property - def speed(self): - """Return the current speed.""" - if self._state: - return AirfreshOperationMode(self._mode).name - - return None - async def async_set_percentage(self, percentage: int) -> None: """Set the percentage of the fan. @@ -1049,23 +955,6 @@ class XiaomiAirFresh(XiaomiGenericDevice): self._mode = self.PRESET_MODE_MAPPING[preset_mode].value self.async_write_ha_state() - # the async_set_speed function is deprecated, support will end with release 2021.7 - # it is added here only for compatibility with legacy speeds - async def async_set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - if self.supported_features & SUPPORT_SET_SPEED == 0: - return - - _LOGGER.debug("Setting the operation mode to: %s", speed) - - if await self._try_command( - "Setting operation mode of the miio device failed.", - self._device.set_mode, - AirfreshOperationMode[speed.title()], - ): - self._mode = AirfreshOperationMode[speed.title()].value - self.async_write_ha_state() - async def async_set_led_on(self): """Turn the led on.""" if self._device_features & FEATURE_SET_LED == 0: From 27849426fe37094516fe78b138a81fa32695e9ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 18 Aug 2021 15:54:11 +0200 Subject: [PATCH 321/355] Remove last_reset attribute and set state class to total_increasing for Integration sensors (#54815) --- homeassistant/components/integration/sensor.py | 13 ++----------- tests/components/integration/test_sensor.py | 17 +++++------------ 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index cabcb2fd394..cf91fd46dad 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 ( - ATTR_LAST_RESET, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.const import ( @@ -28,7 +27,6 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.util import dt as dt_util # mypy: allow-untyped-defs, no-check-untyped-defs @@ -124,25 +122,18 @@ class IntegrationSensor(RestoreEntity, SensorEntity): self._unit_prefix = UNIT_PREFIXES[unit_prefix] self._unit_time = UNIT_TIME[unit_time] - self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_state_class = STATE_CLASS_TOTAL_INCREASING async def async_added_to_hass(self): """Handle entity which will be added.""" await super().async_added_to_hass() state = await self.async_get_last_state() - self._attr_last_reset = dt_util.utcnow() if state: try: self._state = Decimal(state.state) except (DecimalException, ValueError) as err: _LOGGER.warning("Could not restore last state: %s", err) else: - last_reset = dt_util.parse_datetime( - state.attributes.get(ATTR_LAST_RESET, "") - ) - self._attr_last_reset = ( - last_reset if last_reset else dt_util.utc_from_timestamp(0) - ) self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS) self._unit_of_measurement = state.attributes.get( diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 36d3d4b3b30..e8aaf906936 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -2,7 +2,7 @@ from datetime import timedelta from unittest.mock import patch -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import STATE_CLASS_TOTAL_INCREASING from homeassistant.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, @@ -39,8 +39,7 @@ async def test_state(hass) -> None: state = hass.states.get("sensor.integration") assert state is not None - assert state.attributes.get("last_reset") == now.isoformat() - assert state.attributes.get("state_class") == STATE_CLASS_MEASUREMENT + assert state.attributes.get("state_class") == STATE_CLASS_TOTAL_INCREASING assert "device_class" not in state.attributes future_now = dt_util.utcnow() + timedelta(seconds=3600) @@ -58,8 +57,7 @@ async def test_state(hass) -> None: 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_MEASUREMENT - assert state.attributes.get("last_reset") == now.isoformat() + assert state.attributes.get("state_class") == STATE_CLASS_TOTAL_INCREASING async def test_restore_state(hass: HomeAssistant) -> None: @@ -71,7 +69,6 @@ async def test_restore_state(hass: HomeAssistant) -> None: "sensor.integration", "100.0", { - "last_reset": "2019-10-06T21:00:00", "device_class": DEVICE_CLASS_ENERGY, "unit_of_measurement": ENERGY_KILO_WATT_HOUR, }, @@ -97,7 +94,6 @@ async def test_restore_state(hass: HomeAssistant) -> None: 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("last_reset") == "2019-10-06T21:00:00" async def test_restore_state_failed(hass: HomeAssistant) -> None: @@ -108,9 +104,7 @@ async def test_restore_state_failed(hass: HomeAssistant) -> None: State( "sensor.integration", "INVALID", - { - "last_reset": "2019-10-06T21:00:00.000000", - }, + {}, ), ), ) @@ -131,8 +125,7 @@ async def test_restore_state_failed(hass: HomeAssistant) -> None: assert state assert state.state == "0" assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR - assert state.attributes.get("state_class") == STATE_CLASS_MEASUREMENT - assert state.attributes.get("last_reset") != "2019-10-06T21:00:00" + assert state.attributes.get("state_class") == STATE_CLASS_TOTAL_INCREASING assert "device_class" not in state.attributes From 28e421dc5338c7c0d45eb0c2e93501392895bcef Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 15:54:22 +0200 Subject: [PATCH 322/355] Remove `last_reset` attribute and set state class to `total_increasing` for spider energy sensors (#54822) --- homeassistant/components/spider/sensor.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/spider/sensor.py b/homeassistant/components/spider/sensor.py index bf1ab0b18be..8b38fdbe6f6 100644 --- a/homeassistant/components/spider/sensor.py +++ b/homeassistant/components/spider/sensor.py @@ -1,7 +1,9 @@ """Support for Spider Powerplugs (energy & power).""" -from datetime import datetime - -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, +) from homeassistant.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, @@ -9,7 +11,6 @@ from homeassistant.const import ( POWER_WATT, ) from homeassistant.helpers.entity import DeviceInfo -from homeassistant.util import dt as dt_util from .const import DOMAIN @@ -31,7 +32,7 @@ class SpiderPowerPlugEnergy(SensorEntity): _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR _attr_device_class = DEVICE_CLASS_ENERGY - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = STATE_CLASS_TOTAL_INCREASING def __init__(self, api, power_plug) -> None: """Initialize the Spider Power Plug.""" @@ -63,13 +64,6 @@ class SpiderPowerPlugEnergy(SensorEntity): """Return todays energy usage in Kwh.""" return round(self.power_plug.today_energy_consumption / 1000, 2) - @property - def last_reset(self) -> datetime: - """Return the time when last reset; Every midnight.""" - return dt_util.as_utc( - dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) - ) - def update(self) -> None: """Get the latest data.""" self.power_plug = self.api.get_power_plug(self.power_plug.id) From 99477950688ce06be8b99fa4e6a9a2195ce4f939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 18 Aug 2021 17:26:54 +0300 Subject: [PATCH 323/355] Treat Huawei LTE error code 100006 as unsupported functionality (#54253) Internet says 100006 could mean "parameter error", B2368-F20 is reported to respond with that to lan/HostInfo requests. While at it, handle the special case error codes and the "real" not supported exception in the same block. Closes https://github.com/home-assistant/core/issues/53280 --- homeassistant/components/huawei_lte/__init__.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index e220975dbf1..ec9281659f5 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -185,11 +185,6 @@ class Router: _LOGGER.debug("Getting %s for subscribers %s", key, self.subscriptions[key]) try: self.data[key] = func() - except ResponseErrorNotSupportedException: - _LOGGER.info( - "%s not supported by device, excluding from future updates", key - ) - self.subscriptions.pop(key) except ResponseErrorLoginRequiredException: if isinstance(self.connection, AuthorizedConnection): _LOGGER.debug("Trying to authorize again") @@ -206,7 +201,13 @@ class Router: ) self.subscriptions.pop(key) except ResponseErrorException as exc: - if exc.code != -1: + if not isinstance( + exc, ResponseErrorNotSupportedException + ) and exc.code not in ( + # additional codes treated as unusupported + -1, + 100006, + ): raise _LOGGER.info( "%s apparently not supported by device, excluding from future updates", From 4892f6b0945a234ecd449ef7bf3d320e164d9f0e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 16:31:10 +0200 Subject: [PATCH 324/355] Remove `last_reset` attribute and set state class to `total_increasing` for sense energy sensors (#54825) --- homeassistant/components/sense/sensor.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index 69cae55ff31..6be24a73a21 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -1,7 +1,9 @@ """Support for monitoring a Sense energy sensor.""" -import datetime - -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, +) from homeassistant.const import ( ATTR_ATTRIBUTION, DEVICE_CLASS_ENERGY, @@ -12,7 +14,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -import homeassistant.util.dt as dt_util from .const import ( ACTIVE_NAME, @@ -223,7 +224,7 @@ class SenseTrendsSensor(SensorEntity): """Implementation of a Sense energy sensor.""" _attr_device_class = DEVICE_CLASS_ENERGY - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = STATE_CLASS_TOTAL_INCREASING _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_icon = ICON @@ -258,13 +259,6 @@ class SenseTrendsSensor(SensorEntity): """Return if entity is available.""" return self._had_any_update and self._coordinator.last_update_success - @property - def last_reset(self) -> datetime.datetime: - """Return the time when the sensor was last reset, if any.""" - if self._sensor_type == "DAY": - return dt_util.start_of_local_day() - return None - @callback def _async_update(self): """Track if we had an update so we do not report zero data.""" From 6eba04c454fb9c7bdb30bc41953256e4fa232f63 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 16:45:16 +0200 Subject: [PATCH 325/355] Remove last_reset attribute from wemo energy sensors (#54821) --- homeassistant/components/wemo/sensor.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/wemo/sensor.py b/homeassistant/components/wemo/sensor.py index b9d22e6995a..f1f32e8b909 100644 --- a/homeassistant/components/wemo/sensor.py +++ b/homeassistant/components/wemo/sensor.py @@ -1,10 +1,11 @@ """Support for power sensors in WeMo Insight devices.""" import asyncio -from datetime import datetime, timedelta +from datetime import timedelta from typing import Callable from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, ) @@ -16,7 +17,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import StateType -from homeassistant.util import Throttle, convert, dt +from homeassistant.util import Throttle, convert from .const import DOMAIN as WEMO_DOMAIN from .entity import WemoSubscriptionEntity @@ -113,15 +114,10 @@ class InsightTodayEnergy(InsightSensor): key="todaymw", name="Today Energy", device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, unit_of_measurement=ENERGY_KILO_WATT_HOUR, ) - @property - def last_reset(self) -> datetime: - """Return the time when the sensor was initialized.""" - return dt.start_of_local_day() - @property def native_value(self) -> StateType: """Return the current energy use today.""" From 09fbc38baaf95754edf7a3f7720c4eb337557924 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 16:45:30 +0200 Subject: [PATCH 326/355] Remove last_reset attribute from keba energy sensors (#54828) --- homeassistant/components/keba/sensor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/keba/sensor.py b/homeassistant/components/keba/sensor.py index 2c0108ca1cd..a1e0387c707 100644 --- a/homeassistant/components/keba/sensor.py +++ b/homeassistant/components/keba/sensor.py @@ -1,9 +1,12 @@ """Support for KEBA charging station sensors.""" +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, SensorEntity, SensorEntityDescription, ) @@ -12,7 +15,6 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, POWER_KILO_WATT, ) -from homeassistant.util import dt from . import DOMAIN @@ -74,8 +76,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= name="Total Energy", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset=dt.utc_from_timestamp(0), + state_class=STATE_CLASS_TOTAL_INCREASING, ), ), ] From 6aca3b326fa02ab5ba68b8acadd24048b18e07e2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 16:57:19 +0200 Subject: [PATCH 327/355] Remove `last_reset` attribute and set state class to `total_increasing` for fronius energy sensors (#54830) --- homeassistant/components/fronius/sensor.py | 30 ++++++++++++---------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 5141c79f31b..0fb046e8aa1 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.const import ( @@ -32,7 +33,6 @@ from homeassistant.core import 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 -from homeassistant.util import dt _LOGGER = logging.getLogger(__name__) @@ -64,6 +64,17 @@ PREFIX_DEVICE_CLASS_MAPPING = [ ("voltage", DEVICE_CLASS_VOLTAGE), ] +PREFIX_STATE_CLASS_MAPPING = [ + ("state_of_charge", STATE_CLASS_MEASUREMENT), + ("temperature", STATE_CLASS_MEASUREMENT), + ("power_factor", STATE_CLASS_MEASUREMENT), + ("power", STATE_CLASS_MEASUREMENT), + ("energy", STATE_CLASS_TOTAL_INCREASING), + ("current", STATE_CLASS_MEASUREMENT), + ("timestamp", STATE_CLASS_MEASUREMENT), + ("voltage", STATE_CLASS_MEASUREMENT), +] + def _device_id_validator(config): """Ensure that inverters have default id 1 and other devices 0.""" @@ -281,8 +292,6 @@ class FroniusPowerFlow(FroniusAdapter): class FroniusTemplateSensor(SensorEntity): """Sensor for the single values (e.g. pv power, ac power).""" - _attr_state_class = STATE_CLASS_MEASUREMENT - def __init__(self, parent: FroniusAdapter, key: str) -> None: """Initialize a singular value sensor.""" self._key = key @@ -292,6 +301,10 @@ class FroniusTemplateSensor(SensorEntity): if self._key.startswith(prefix): self._attr_device_class = device_class break + for prefix, state_class in PREFIX_STATE_CLASS_MAPPING: + if self._key.startswith(prefix): + self._attr_state_class = state_class + break @property def should_poll(self): @@ -311,17 +324,6 @@ class FroniusTemplateSensor(SensorEntity): self._attr_native_value = round(self._attr_native_value, 2) self._attr_native_unit_of_measurement = state.get("unit") - @property - def last_reset(self) -> dt.dt.datetime | None: - """Return the time when the sensor was last reset, if it is a meter.""" - if self._key.endswith("day"): - return dt.start_of_local_day() - if self._key.endswith("year"): - return dt.start_of_local_day(dt.dt.date(dt.now().year, 1, 1)) - if self._key.endswith("total") or self._key.startswith("energy_real"): - return dt.utc_from_timestamp(0) - return None - async def async_added_to_hass(self): """Register at parent component for updates.""" self.async_on_remove(self._parent.register(self)) From 9c7ea786a786bf917080e04aa006b61bcd24407c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 16:57:38 +0200 Subject: [PATCH 328/355] Remove `last_reset` attribute and set state class to `total_increasing` for saj energy sensors (#54813) Co-authored-by: Franck Nijhof --- homeassistant/components/saj/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 795823b9e9f..8e59899de27 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.const import ( @@ -34,7 +35,6 @@ from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_call_later -from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -177,10 +177,10 @@ class SAJsensor(SensorEntity): self._serialnumber = serialnumber self._state = self._sensor.value - if pysaj_sensor.name in ("current_power", "total_yield", "temperature"): + if pysaj_sensor.name in ("current_power", "temperature"): self._attr_state_class = STATE_CLASS_MEASUREMENT if pysaj_sensor.name == "total_yield": - self._attr_last_reset = dt_util.utc_from_timestamp(0) + self._attr_state_class = STATE_CLASS_TOTAL_INCREASING @property def name(self): From e98d50f6d1e6012bcd4d0735f1476306851fc305 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 16:58:13 +0200 Subject: [PATCH 329/355] Remove `last_reset` attribute and set state class to `total_increasing` for mysensors energy sensors (#54827) --- homeassistant/components/mysensors/sensor.py | 5 ++--- tests/components/mysensors/test_sensor.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 68fdf2a21b2..94a9cde1df2 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -9,6 +9,7 @@ from homeassistant.components import mysensors from homeassistant.components.sensor import ( DOMAIN, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, ) @@ -42,7 +43,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util.dt import utc_from_timestamp from .const import MYSENSORS_DISCOVERY, DiscoveryInfo from .helpers import on_unload @@ -122,8 +122,7 @@ SENSORS: dict[str, SensorEntityDescription] = { key="V_KWH", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset=utc_from_timestamp(0), + state_class=STATE_CLASS_TOTAL_INCREASING, ), "V_LIGHT_LEVEL": SensorEntityDescription( key="V_LIGHT_LEVEL", diff --git a/tests/components/mysensors/test_sensor.py b/tests/components/mysensors/test_sensor.py index 880226ced60..18d88a24206 100644 --- a/tests/components/mysensors/test_sensor.py +++ b/tests/components/mysensors/test_sensor.py @@ -10,6 +10,7 @@ from homeassistant.components.sensor import ( ATTR_LAST_RESET, ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -24,7 +25,6 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant -from homeassistant.util.dt import utc_from_timestamp from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem from tests.common import MockConfigEntry @@ -92,8 +92,7 @@ async def test_energy_sensor( assert state.state == "18000" assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_KILO_WATT_HOUR - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT - assert state.attributes[ATTR_LAST_RESET] == utc_from_timestamp(0).isoformat() + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING async def test_sound_sensor( From a6ac55390a3d6c041de17921d44c2c482d39058e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 17:14:02 +0200 Subject: [PATCH 330/355] Remove `last_reset` attribute and set state class to `total_increasing` for smartthings energy sensors (#54824) Co-authored-by: Franck Nijhof --- .../components/smartthings/sensor.py | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index a8e6c0472e9..7c682486f04 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -3,12 +3,15 @@ from __future__ import annotations from collections import namedtuple from collections.abc import Sequence -from datetime import datetime from pysmartthings import Attribute, Capability from pysmartthings.device import DeviceEntity -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, +) from homeassistant.const import ( AREA_SQUARE_METERS, CONCENTRATION_PARTS_PER_MILLION, @@ -33,7 +36,6 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, VOLUME_CUBIC_METERS, ) -from homeassistant.util.dt import utc_from_timestamp from . import SmartThingsEntity from .const import DATA_BROKERS, DOMAIN @@ -133,7 +135,7 @@ CAPABILITY_TO_SENSORS = { "Energy Meter", ENERGY_KILO_WATT_HOUR, DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, ) ], Capability.equivalent_carbon_dioxide_measurement: [ @@ -507,13 +509,6 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): unit = self._device.status.attributes[self._attribute].unit return UNITS.get(unit, unit) if unit else self._default_unit - @property - def last_reset(self) -> datetime | None: - """Return the time when the sensor was last reset, if any.""" - if self._attribute == Attribute.energy: - return utc_from_timestamp(0) - return None - class SmartThingsThreeAxisSensor(SmartThingsEntity, SensorEntity): """Define a SmartThings Three Axis Sensor.""" @@ -554,8 +549,9 @@ class SmartThingsPowerConsumptionSensor(SmartThingsEntity, SensorEntity): """Init the class.""" super().__init__(device) self.report_name = report_name - # This is an exception for STATE_CLASS_MEASUREMENT per @balloob self._attr_state_class = STATE_CLASS_MEASUREMENT + if self.report_name != "power": + self._attr_state_class = STATE_CLASS_TOTAL_INCREASING @property def name(self) -> str: @@ -590,10 +586,3 @@ class SmartThingsPowerConsumptionSensor(SmartThingsEntity, SensorEntity): if self.report_name == "power": return POWER_WATT return ENERGY_KILO_WATT_HOUR - - @property - def last_reset(self) -> datetime | None: - """Return the time when the sensor was last reset, if any.""" - if self.report_name != "power": - return utc_from_timestamp(0) - return None From c1595d5ceb7fbf7d097ef83d6c28ba15bd8ebb3a Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 18 Aug 2021 11:53:00 -0400 Subject: [PATCH 331/355] Only show zwave_js command classes that are on the node (#54794) --- .../components/zwave_js/device_condition.py | 5 ++++- tests/components/zwave_js/test_device_condition.py | 13 ++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/device_condition.py b/homeassistant/components/zwave_js/device_condition.py index b419230a0bd..4ae8142ec9e 100644 --- a/homeassistant/components/zwave_js/device_condition.py +++ b/homeassistant/components/zwave_js/device_condition.py @@ -197,7 +197,10 @@ async def async_get_condition_capabilities( "extra_fields": vol.Schema( { vol.Required(ATTR_COMMAND_CLASS): vol.In( - {cc.value: cc.name for cc in CommandClass} + { + CommandClass(cc.id).value: CommandClass(cc.id).name + for cc in sorted(node.command_classes, key=lambda cc: cc.name) # type: ignore[no-any-return] + } ), vol.Required(ATTR_PROPERTY): cv.string, vol.Optional(ATTR_PROPERTY_KEY): cv.string, diff --git a/tests/components/zwave_js/test_device_condition.py b/tests/components/zwave_js/test_device_condition.py index eef672c4c5b..0256981a726 100644 --- a/tests/components/zwave_js/test_device_condition.py +++ b/tests/components/zwave_js/test_device_condition.py @@ -430,7 +430,18 @@ async def test_get_condition_capabilities_value( ) assert capabilities and "extra_fields" in capabilities - cc_options = [(cc.value, cc.name) for cc in CommandClass] + cc_options = [ + (133, "ASSOCIATION"), + (128, "BATTERY"), + (112, "CONFIGURATION"), + (98, "DOOR_LOCK"), + (122, "FIRMWARE_UPDATE_MD"), + (114, "MANUFACTURER_SPECIFIC"), + (113, "ALARM"), + (152, "SECURITY"), + (99, "USER_CODE"), + (134, "VERSION"), + ] assert voluptuous_serialize.convert( capabilities["extra_fields"], custom_serializer=cv.custom_serializer From 041ba2ec3a179594d55b0a3c683f8177a43031f9 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Wed, 18 Aug 2021 17:58:07 +0200 Subject: [PATCH 332/355] Fix BMW remote services in rest_of_world & north_america (#54726) 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 8131ac1415c..a7c4c5c837b 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.7.18"], + "requirements": ["bimmer_connected==0.7.19"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 0eb8046c327..021d225c362 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -371,7 +371,7 @@ beautifulsoup4==4.9.3 bellows==0.26.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.18 +bimmer_connected==0.7.19 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd478a18538..ecb66f6bad4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -226,7 +226,7 @@ base36==0.1.1 bellows==0.26.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.18 +bimmer_connected==0.7.19 # homeassistant.components.blebox blebox_uniapi==1.3.3 From bce7c73925b3ffed6978dd342a120dfd7b7c171f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 17:58:32 +0200 Subject: [PATCH 333/355] Remove `last_reset` attribute from and set state class to `total_increasing` for enphase_envoy energy sensors (#54831) --- homeassistant/components/enphase_envoy/const.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index 1d0dfba8990..ff42ef23746 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -3,10 +3,10 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntityDescription, ) from homeassistant.const import DEVICE_CLASS_ENERGY, ENERGY_WATT_HOUR, POWER_WATT -from homeassistant.util import dt DOMAIN = "enphase_envoy" @@ -41,9 +41,8 @@ SENSORS = ( key="lifetime_production", name="Lifetime Energy Production", native_unit_of_measurement=ENERGY_WATT_HOUR, - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, device_class=DEVICE_CLASS_ENERGY, - last_reset=dt.utc_from_timestamp(0), ), SensorEntityDescription( key="consumption", @@ -69,9 +68,8 @@ SENSORS = ( key="lifetime_consumption", name="Lifetime Energy Consumption", native_unit_of_measurement=ENERGY_WATT_HOUR, - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_TOTAL_INCREASING, device_class=DEVICE_CLASS_ENERGY, - last_reset=dt.utc_from_timestamp(0), ), SensorEntityDescription( key="inverters", From 8d37fd08c79f5cc4acbc3e1f5c026b8abcb173f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Lov=C3=A9n?= Date: Wed, 18 Aug 2021 17:59:31 +0200 Subject: [PATCH 334/355] Fix integration sensors sometimes not getting device_class or unit_of_measurement (#54802) --- homeassistant/components/integration/sensor.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index cf91fd46dad..b8e72c3be5c 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -145,12 +145,6 @@ class IntegrationSensor(RestoreEntity, SensorEntity): """Handle the sensor state changes.""" old_state = event.data.get("old_state") new_state = event.data.get("new_state") - if ( - old_state is None - or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) - or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) - ): - return if self._unit_of_measurement is None: unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -162,6 +156,14 @@ class IntegrationSensor(RestoreEntity, SensorEntity): and new_state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER ): self._attr_device_class = DEVICE_CLASS_ENERGY + + if ( + old_state is None + or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) + or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) + ): + return + try: # integration as the Riemann integral of previous measures. area = 0 From 5d19575a846d8cc6d94bccb4e645896c0de870e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Aug 2021 11:00:09 -0500 Subject: [PATCH 335/355] Exclude global scope IPv6 when setting up zeroconf interfaces (#54632) --- homeassistant/components/zeroconf/__init__.py | 10 +++++++--- tests/components/zeroconf/test_init.py | 12 ++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index a85236b6a07..6829c9c5e17 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -162,10 +162,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: interfaces.extend( ipv4["address"] for ipv4 in ipv4s - if not ipaddress.ip_address(ipv4["address"]).is_loopback + if not ipaddress.IPv4Address(ipv4["address"]).is_loopback ) - if adapter["ipv6"] and adapter["index"] not in interfaces: - interfaces.append(adapter["index"]) + if ipv6s := adapter["ipv6"]: + for ipv6_addr in ipv6s: + address = ipv6_addr["address"] + v6_ip_address = ipaddress.IPv6Address(address) + if not v6_ip_address.is_global and not v6_ip_address.is_loopback: + interfaces.append(ipv6_addr["address"]) aio_zc = await _async_get_instance(hass, **zc_args) zeroconf = cast(HaZeroconf, aio_zc.zeroconf) diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 0db8f0f5227..a284c91e4f4 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -799,7 +799,14 @@ async def test_async_detect_interfaces_setting_empty_route(hass, mock_async_zero hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert mock_zc.mock_calls[0] == call( - interfaces=[1, "192.168.1.5", "172.16.1.5", 3], ip_version=IPVersion.All + interfaces=[ + "2001:db8::", + "fe80::1234:5678:9abc:def0", + "192.168.1.5", + "172.16.1.5", + "fe80::dead:beef:dead:beef", + ], + ip_version=IPVersion.All, ) @@ -862,5 +869,6 @@ async def test_async_detect_interfaces_explicitly_set_ipv6(hass, mock_async_zero await hass.async_block_till_done() assert mock_zc.mock_calls[0] == call( - interfaces=["192.168.1.5", 1], ip_version=IPVersion.All + interfaces=["192.168.1.5", "fe80::dead:beef:dead:beef"], + ip_version=IPVersion.All, ) From bca9360d523a173bffad465b118f0197c163c38a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 18:25:33 +0200 Subject: [PATCH 336/355] Remove last_reset attribute from tasmota energy sensors (#54836) --- homeassistant/components/tasmota/sensor.py | 24 ++++++---------------- tests/components/tasmota/test_sensor.py | 5 +++-- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 29144370ae7..39ee97d1648 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import datetime -import logging from typing import Any from hatasmota import const as hc, sensor as tasmota_sensor, status_sensor @@ -10,7 +9,11 @@ from hatasmota.entity import TasmotaEntity as HATasmotaEntity from hatasmota.models import DiscoveryHashType from homeassistant.components import sensor -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, @@ -49,14 +52,11 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import dt as dt_util from .const import DATA_REMOVE_DISCOVER_COMPONENT from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate -_LOGGER = logging.getLogger(__name__) - DEVICE_CLASS = "device_class" STATE_CLASS = "state_class" ICON = "icon" @@ -121,7 +121,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP = { hc.SENSOR_TODAY: {DEVICE_CLASS: DEVICE_CLASS_ENERGY}, hc.SENSOR_TOTAL: { DEVICE_CLASS: DEVICE_CLASS_ENERGY, - STATE_CLASS: STATE_CLASS_MEASUREMENT, + STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, hc.SENSOR_TVOC: {ICON: "mdi:air-filter"}, @@ -188,7 +188,6 @@ async def async_setup_entry( class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): """Representation of a Tasmota sensor.""" - _attr_last_reset = None _tasmota_entity: tasmota_sensor.TasmotaSensor def __init__(self, **kwds: Any) -> None: @@ -212,17 +211,6 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): self._state_timestamp = state else: self._state = state - if "last_reset" in kwargs: - try: - last_reset_dt = dt_util.parse_datetime(kwargs["last_reset"]) - last_reset = dt_util.as_utc(last_reset_dt) if last_reset_dt else None - if last_reset is None: - raise ValueError - self._attr_last_reset = last_reset - except ValueError: - _LOGGER.warning( - "Invalid last_reset timestamp '%s'", kwargs["last_reset"] - ) self.async_write_ha_state() @property diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index fc1e7fd624b..adb73dcf334 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -255,6 +255,9 @@ 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 + ) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") state = hass.states.get("sensor.tasmota_energy_total") @@ -269,7 +272,6 @@ async def test_indexed_sensor_state_via_mqtt2(hass, mqtt_mock, setup_tasmota): ) state = hass.states.get("sensor.tasmota_energy_total") assert state.state == "1.2" - assert state.attributes["last_reset"] == "2018-11-23T15:33:47+00:00" # Test polled state update async_fire_mqtt_message( @@ -279,7 +281,6 @@ async def test_indexed_sensor_state_via_mqtt2(hass, mqtt_mock, setup_tasmota): ) state = hass.states.get("sensor.tasmota_energy_total") assert state.state == "5.6" - assert state.attributes["last_reset"] == "2018-11-23T16:33:47+00:00" async def test_bad_indexed_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota): From e7a0604a40921cdbf151521b4f8555e008504391 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Aug 2021 11:36:13 -0500 Subject: [PATCH 337/355] Make yeelight discovery async (#54711) --- homeassistant/components/yeelight/__init__.py | 271 +++++++++++------- .../components/yeelight/config_flow.py | 82 +++--- homeassistant/components/yeelight/light.py | 4 +- .../components/yeelight/manifest.json | 2 +- requirements_all.txt | 1 + requirements_test_all.txt | 1 + tests/components/yeelight/__init__.py | 58 +++- .../components/yeelight/test_binary_sensor.py | 13 +- tests/components/yeelight/test_config_flow.py | 175 ++++++++--- tests/components/yeelight/test_init.py | 112 ++++---- tests/components/yeelight/test_light.py | 17 +- 11 files changed, 478 insertions(+), 258 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index c9e654bca9a..0ea4eb8e84f 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -2,13 +2,17 @@ from __future__ import annotations import asyncio +import contextlib from datetime import timedelta import logging +from urllib.parse import urlparse +from async_upnp_client.search import SSDPListener import voluptuous as vol -from yeelight import BulbException, discover_bulbs +from yeelight import BulbException from yeelight.aio import KEY_CONNECTED, AsyncBulb +from homeassistant import config_entries from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryNotReady from homeassistant.const import ( CONF_DEVICES, @@ -24,6 +28,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.event import async_call_later, async_track_time_interval from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -69,6 +74,12 @@ ACTIVE_COLOR_FLOWING = "1" NIGHTLIGHT_SWITCH_TYPE_LIGHT = "light" DISCOVERY_INTERVAL = timedelta(seconds=60) +SSDP_TARGET = ("239.255.255.250", 1982) +SSDP_ST = "wifi_bulb" +DISCOVERY_ATTEMPTS = 3 +DISCOVERY_SEARCH_INTERVAL = timedelta(seconds=2) +DISCOVERY_TIMEOUT = 2 + YEELIGHT_RGB_TRANSITION = "RGBTransition" YEELIGHT_HSV_TRANSACTION = "HSVTransition" @@ -193,20 +204,12 @@ async def _async_initialize( hass.config_entries.async_setup_platforms(entry, PLATFORMS) if not device: + # get device and start listening for local pushes device = await _async_get_device(hass, host, entry) + + await device.async_setup() entry_data[DATA_DEVICE] = device - # start listening for local pushes - await device.bulb.async_listen(device.async_update_callback) - - # register stop callback to shutdown listening for local pushes - async def async_stop_listen_task(event): - """Stop listen thread.""" - _LOGGER.debug("Shutting down Yeelight Listener") - await device.bulb.async_stop_listening() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_listen_task) - entry.async_on_unload( async_dispatcher_connect( hass, DEVICE_INITIALIZED.format(host), _async_load_platforms @@ -251,7 +254,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if entry.data.get(CONF_HOST): try: device = await _async_get_device(hass, entry.data[CONF_HOST], entry) - except OSError as ex: + except BulbException as ex: # If CONF_ID is not valid we cannot fallback to discovery # so we must retry by raising ConfigEntryNotReady if not entry.data.get(CONF_ID): @@ -267,16 +270,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from ex return True - # discovery - scanner = YeelightScanner.async_get(hass) - - async def _async_from_discovery(host: str) -> None: + async def _async_from_discovery(capabilities: dict[str, str]) -> None: + host = urlparse(capabilities["location"]).hostname try: await _async_initialize(hass, entry, host) except BulbException: _LOGGER.exception("Failed to connect to bulb at %s", host) - scanner.async_register_callback(entry.data[CONF_ID], _async_from_discovery) + # discovery + scanner = YeelightScanner.async_get(hass) + await scanner.async_register_callback(entry.data[CONF_ID], _async_from_discovery) return True @@ -294,10 +297,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: scanner = YeelightScanner.async_get(hass) scanner.async_unregister_callback(entry.data[CONF_ID]) - device = entry_data[DATA_DEVICE] - _LOGGER.debug("Shutting down Yeelight Listener") - await device.bulb.async_stop_listening() - _LOGGER.debug("Yeelight Listener stopped") + if DATA_DEVICE in entry_data: + device = entry_data[DATA_DEVICE] + _LOGGER.debug("Shutting down Yeelight Listener") + await device.bulb.async_stop_listening() + _LOGGER.debug("Yeelight Listener stopped") data_config_entries.pop(entry.entry_id) @@ -307,9 +311,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @callback def _async_unique_name(capabilities: dict) -> str: """Generate name from capabilities.""" - model = capabilities["model"] - unique_id = capabilities["id"] - return f"yeelight_{model}_{unique_id}" + model = str(capabilities["model"]).replace("_", " ").title() + short_id = hex(int(capabilities["id"], 16)) + return f"Yeelight {model} {short_id}" async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): @@ -333,88 +337,147 @@ class YeelightScanner: def __init__(self, hass: HomeAssistant) -> None: """Initialize class.""" self._hass = hass - self._seen = {} self._callbacks = {} - self._scan_task = None + self._host_discovered_events = {} + self._unique_id_capabilities = {} + self._host_capabilities = {} + self._track_interval = None + self._listener = None + self._connected_event = None - async def _async_scan(self): - _LOGGER.debug("Yeelight scanning") - # Run 3 times as packets can get lost - for _ in range(3): - devices = await self._hass.async_add_executor_job(discover_bulbs) - for device in devices: - unique_id = device["capabilities"]["id"] - if unique_id in self._seen: - continue - host = device["ip"] - self._seen[unique_id] = host - _LOGGER.debug("Yeelight discovered at %s", host) - if unique_id in self._callbacks: - self._hass.async_create_task(self._callbacks[unique_id](host)) - self._callbacks.pop(unique_id) - if len(self._callbacks) == 0: - self._async_stop_scan() + async def async_setup(self): + """Set up the scanner.""" + if self._connected_event: + await self._connected_event.wait() + return + self._connected_event = asyncio.Event() - await asyncio.sleep(DISCOVERY_INTERVAL.total_seconds()) - self._scan_task = self._hass.loop.create_task(self._async_scan()) + async def _async_connected(): + self._listener.async_search() + self._connected_event.set() + + self._listener = SSDPListener( + async_callback=self._async_process_entry, + service_type=SSDP_ST, + target=SSDP_TARGET, + async_connect_callback=_async_connected, + ) + await self._listener.async_start() + await self._connected_event.wait() + + async def async_discover(self): + """Discover bulbs.""" + await self.async_setup() + for _ in range(DISCOVERY_ATTEMPTS): + self._listener.async_search() + await asyncio.sleep(DISCOVERY_SEARCH_INTERVAL.total_seconds()) + return self._unique_id_capabilities.values() @callback - def _async_start_scan(self): + def async_scan(self, *_): + """Send discovery packets.""" + _LOGGER.debug("Yeelight scanning") + self._listener.async_search() + + async def async_get_capabilities(self, host): + """Get capabilities via SSDP.""" + if host in self._host_capabilities: + return self._host_capabilities[host] + + host_event = asyncio.Event() + self._host_discovered_events.setdefault(host, []).append(host_event) + await self.async_setup() + + self._listener.async_search((host, SSDP_TARGET[1])) + + with contextlib.suppress(asyncio.TimeoutError): + await asyncio.wait_for(host_event.wait(), timeout=DISCOVERY_TIMEOUT) + + self._host_discovered_events[host].remove(host_event) + return self._host_capabilities.get(host) + + def _async_discovered_by_ssdp(self, response): + @callback + def _async_start_flow(*_): + asyncio.create_task( + self._hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=response, + ) + ) + + # Delay starting the flow in case the discovery is the result + # of another discovery + async_call_later(self._hass, 1, _async_start_flow) + + async def _async_process_entry(self, response): + """Process a discovery.""" + _LOGGER.debug("Discovered via SSDP: %s", response) + unique_id = response["id"] + host = urlparse(response["location"]).hostname + if unique_id not in self._unique_id_capabilities: + _LOGGER.debug("Yeelight discovered with %s", response) + self._async_discovered_by_ssdp(response) + self._host_capabilities[host] = response + self._unique_id_capabilities[unique_id] = response + for event in self._host_discovered_events.get(host, []): + event.set() + if unique_id in self._callbacks: + self._hass.async_create_task(self._callbacks[unique_id](response)) + self._callbacks.pop(unique_id) + if not self._callbacks: + self._async_stop_scan() + + async def _async_start_scan(self): """Start scanning for Yeelight devices.""" _LOGGER.debug("Start scanning") - # Use loop directly to avoid home assistant track this task - self._scan_task = self._hass.loop.create_task(self._async_scan()) + await self.async_setup() + if not self._track_interval: + self._track_interval = async_track_time_interval( + self._hass, self.async_scan, DISCOVERY_INTERVAL + ) + self.async_scan() @callback def _async_stop_scan(self): """Stop scanning.""" - _LOGGER.debug("Stop scanning") - if self._scan_task is not None: - self._scan_task.cancel() - self._scan_task = None + if self._track_interval is None: + return + _LOGGER.debug("Stop scanning interval") + self._track_interval() + self._track_interval = None - @callback - def async_register_callback(self, unique_id, callback_func): + async def async_register_callback(self, unique_id, callback_func): """Register callback function.""" - host = self._seen.get(unique_id) - if host is not None: - self._hass.async_create_task(callback_func(host)) - else: - self._callbacks[unique_id] = callback_func - if len(self._callbacks) == 1: - self._async_start_scan() + if capabilities := self._unique_id_capabilities.get(unique_id): + self._hass.async_create_task(callback_func(capabilities)) + return + self._callbacks[unique_id] = callback_func + await self._async_start_scan() @callback def async_unregister_callback(self, unique_id): """Unregister callback function.""" - if unique_id not in self._callbacks: - return - self._callbacks.pop(unique_id) - if len(self._callbacks) == 0: + self._callbacks.pop(unique_id, None) + if not self._callbacks: self._async_stop_scan() class YeelightDevice: """Represents single Yeelight device.""" - def __init__(self, hass, host, config, bulb, capabilities): + def __init__(self, hass, host, config, bulb): """Initialize device.""" self._hass = hass self._config = config self._host = host self._bulb_device = bulb - self._capabilities = capabilities or {} + self._capabilities = {} self._device_type = None self._available = False self._initialized = False - - self._name = host # Default name is host - if capabilities: - # Generate name from model and id when capabilities is available - self._name = _async_unique_name(capabilities) - if config.get(CONF_NAME): - # Override default name when name is set in config - self._name = config[CONF_NAME] + self._name = None @property def bulb(self): @@ -444,7 +507,7 @@ class YeelightDevice: @property def model(self): """Return configured/autodetected device model.""" - return self._bulb_device.model + return self._bulb_device.model or self._capabilities.get("model") @property def fw_version(self): @@ -530,7 +593,8 @@ class YeelightDevice: await self.bulb.async_get_properties(UPDATE_REQUEST_PROPERTIES) self._available = True if not self._initialized: - await self._async_initialize_device() + self._initialized = True + async_dispatcher_send(self._hass, DEVICE_INITIALIZED.format(self._host)) except BulbException as ex: if self._available: # just inform once _LOGGER.error( @@ -540,28 +604,18 @@ class YeelightDevice: return self._available - async def _async_get_capabilities(self): - """Request device capabilities.""" - try: - await self._hass.async_add_executor_job(self.bulb.get_capabilities) - _LOGGER.debug( - "Device %s, %s capabilities: %s", - self._host, - self.name, - self.bulb.capabilities, - ) - except BulbException as ex: - _LOGGER.error( - "Unable to get device capabilities %s, %s: %s", - self._host, - self.name, - ex, - ) - - async def _async_initialize_device(self): - await self._async_get_capabilities() - self._initialized = True - async_dispatcher_send(self._hass, DEVICE_INITIALIZED.format(self._host)) + async def async_setup(self): + """Fetch capabilities and setup name if available.""" + scanner = YeelightScanner.async_get(self._hass) + self._capabilities = await scanner.async_get_capabilities(self._host) or {} + if name := self._config.get(CONF_NAME): + # Override default name when name is set in config + self._name = name + elif self._capabilities: + # Generate name from model and id when capabilities is available + self._name = _async_unique_name(self._capabilities) + else: + self._name = self._host # Default name is host async def async_update(self): """Update device properties and send data updated signal.""" @@ -628,6 +682,19 @@ async def _async_get_device( # Set up device bulb = AsyncBulb(host, model=model or None) - capabilities = await hass.async_add_executor_job(bulb.get_capabilities) - return YeelightDevice(hass, host, entry.options, bulb, capabilities) + device = YeelightDevice(hass, host, entry.options, bulb) + # start listening for local pushes + await device.bulb.async_listen(device.async_update_callback) + + # register stop callback to shutdown listening for local pushes + async def async_stop_listen_task(event): + """Stop listen thread.""" + _LOGGER.debug("Shutting down Yeelight Listener") + await device.bulb.async_stop_listening() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_listen_task) + ) + + return device diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index a66571cae93..d93f59535cf 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -1,8 +1,10 @@ """Config flow for Yeelight integration.""" import logging +from urllib.parse import urlparse import voluptuous as vol import yeelight +from yeelight.aio import AsyncBulb from homeassistant import config_entries, exceptions from homeassistant.components.dhcp import IP_ADDRESS @@ -19,6 +21,7 @@ from . import ( CONF_TRANSITION, DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, + YeelightScanner, _async_unique_name, ) @@ -54,6 +57,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._discovered_ip = discovery_info[IP_ADDRESS] return await self._async_handle_discovery() + async def async_step_ssdp(self, discovery_info): + """Handle discovery from ssdp.""" + self._discovered_ip = urlparse(discovery_info["location"]).hostname + await self.async_set_unique_id(discovery_info["id"]) + self._abort_if_unique_id_configured( + updates={CONF_HOST: self._discovered_ip}, reload_on_update=False + ) + return await self._async_handle_discovery() + async def _async_handle_discovery(self): """Handle any discovery.""" self.context[CONF_HOST] = self._discovered_ip @@ -62,7 +74,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_in_progress") try: - self._discovered_model = await self._async_try_connect(self._discovered_ip) + self._discovered_model = await self._async_try_connect( + self._discovered_ip, raise_on_progress=True + ) except CannotConnect: return self.async_abort(reason="cannot_connect") @@ -96,7 +110,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not user_input.get(CONF_HOST): return await self.async_step_pick_device() try: - model = await self._async_try_connect(user_input[CONF_HOST]) + model = await self._async_try_connect( + user_input[CONF_HOST], raise_on_progress=False + ) except CannotConnect: errors["base"] = "cannot_connect" else: @@ -119,10 +135,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: unique_id = user_input[CONF_DEVICE] capabilities = self._discovered_devices[unique_id] - await self.async_set_unique_id(unique_id) + await self.async_set_unique_id(unique_id, raise_on_progress=False) self._abort_if_unique_id_configured() + host = urlparse(capabilities["location"]).hostname return self.async_create_entry( - title=_async_unique_name(capabilities), data={CONF_ID: unique_id} + title=_async_unique_name(capabilities), + data={CONF_ID: unique_id, CONF_HOST: host}, ) configured_devices = { @@ -131,19 +149,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if entry.data[CONF_ID] } devices_name = {} + scanner = YeelightScanner.async_get(self.hass) + devices = await scanner.async_discover() # Run 3 times as packets can get lost - for _ in range(3): - devices = await self.hass.async_add_executor_job(yeelight.discover_bulbs) - for device in devices: - capabilities = device["capabilities"] - unique_id = capabilities["id"] - if unique_id in configured_devices: - continue # ignore configured devices - model = capabilities["model"] - host = device["ip"] - name = f"{host} {model} {unique_id}" - self._discovered_devices[unique_id] = capabilities - devices_name[unique_id] = name + for capabilities in devices: + unique_id = capabilities["id"] + if unique_id in configured_devices: + continue # ignore configured devices + model = capabilities["model"] + host = urlparse(capabilities["location"]).hostname + name = f"{host} {model} {unique_id}" + self._discovered_devices[unique_id] = capabilities + devices_name[unique_id] = name # Check if there is at least one device if not devices_name: @@ -157,7 +174,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle import step.""" host = user_input[CONF_HOST] try: - await self._async_try_connect(host) + await self._async_try_connect(host, raise_on_progress=False) except CannotConnect: _LOGGER.error("Failed to import %s: cannot connect", host) return self.async_abort(reason="cannot_connect") @@ -169,27 +186,26 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) - async def _async_try_connect(self, host): + async def _async_try_connect(self, host, raise_on_progress=True): """Set up with options.""" self._async_abort_entries_match({CONF_HOST: host}) - bulb = yeelight.Bulb(host) - try: - capabilities = await self.hass.async_add_executor_job(bulb.get_capabilities) - if capabilities is None: # timeout - _LOGGER.debug("Failed to get capabilities from %s: timeout", host) - else: - _LOGGER.debug("Get capabilities: %s", capabilities) - await self.async_set_unique_id(capabilities["id"]) - return capabilities["model"] - except OSError as err: - _LOGGER.debug("Failed to get capabilities from %s: %s", host, err) - # Ignore the error since get_capabilities uses UDP discovery packet - # which does not work in all network environments - + scanner = YeelightScanner.async_get(self.hass) + capabilities = await scanner.async_get_capabilities(host) + if capabilities is None: # timeout + _LOGGER.debug("Failed to get capabilities from %s: timeout", host) + else: + _LOGGER.debug("Get capabilities: %s", capabilities) + await self.async_set_unique_id( + capabilities["id"], raise_on_progress=raise_on_progress + ) + return capabilities["model"] # Fallback to get properties + bulb = AsyncBulb(host) try: - await self.hass.async_add_executor_job(bulb.get_properties) + await bulb.async_listen(lambda _: True) + await bulb.async_get_properties() + await bulb.async_stop_listening() except yeelight.BulbException as err: _LOGGER.error("Failed to get properties from %s: %s", host, err) raise CannotConnect from err diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index b714ddfaba8..4766d897909 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -905,7 +905,7 @@ class YeelightNightLightMode(YeelightGenericLight): @property def name(self) -> str: """Return the name of the device if any.""" - return f"{self.device.name} nightlight" + return f"{self.device.name} Nightlight" @property def icon(self): @@ -997,7 +997,7 @@ class YeelightAmbientLight(YeelightColorLightWithoutNightlightSwitch): @property def name(self) -> str: """Return the name of the device if any.""" - return f"{self.device.name} ambilight" + return f"{self.device.name} Ambilight" @property def _brightness_property(self): diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 3528b096c67..4c5994b1f6e 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.2"], + "requirements": ["yeelight==0.7.2", "async-upnp-client==0.20.0"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 021d225c362..69da5abc72d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -314,6 +314,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp # homeassistant.components.upnp +# homeassistant.components.yeelight async-upnp-client==0.20.0 # homeassistant.components.supla diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ecb66f6bad4..e476881426c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -205,6 +205,7 @@ arcam-fmj==0.7.0 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp # homeassistant.components.upnp +# homeassistant.components.yeelight async-upnp-client==0.20.0 # homeassistant.components.aurora diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index 9fa864d6213..cb2936cf8e2 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -1,9 +1,13 @@ """Tests for the Yeelight integration.""" +import asyncio +from datetime import timedelta from unittest.mock import AsyncMock, MagicMock, patch +from async_upnp_client.search import SSDPListener from yeelight import BulbException, BulbType from yeelight.main import _MODEL_SPECS +from homeassistant.components import yeelight as hass_yeelight from homeassistant.components.yeelight import ( CONF_MODE_MUSIC, CONF_NIGHTLIGHT_SWITCH_TYPE, @@ -13,6 +17,7 @@ from homeassistant.components.yeelight import ( YeelightScanner, ) from homeassistant.const import CONF_DEVICES, CONF_ID, CONF_NAME +from homeassistant.core import callback IP_ADDRESS = "192.168.1.239" MODEL = "color" @@ -23,13 +28,16 @@ CAPABILITIES = { "id": ID, "model": MODEL, "fw_ver": FW_VER, + "location": f"yeelight://{IP_ADDRESS}", "support": "get_prop set_default set_power toggle set_bright start_cf stop_cf" " set_scene cron_add cron_get cron_del set_ct_abx set_rgb", "name": "", } NAME = "name" -UNIQUE_NAME = f"yeelight_{MODEL}_{ID}" +SHORT_ID = hex(int("0x000000000015243f", 16)) +UNIQUE_NAME = f"yeelight_{MODEL}_{SHORT_ID}" +UNIQUE_FRIENDLY_NAME = f"Yeelight {MODEL.title()} {SHORT_ID}" MODULE = "homeassistant.components.yeelight" MODULE_CONFIG_FLOW = f"{MODULE}.config_flow" @@ -81,8 +89,8 @@ CONFIG_ENTRY_DATA = {CONF_ID: ID} def _mocked_bulb(cannot_connect=False): bulb = MagicMock() - type(bulb).get_capabilities = MagicMock( - return_value=None if cannot_connect else CAPABILITIES + type(bulb).async_listen = AsyncMock( + side_effect=BulbException if cannot_connect else None ) type(bulb).async_get_properties = AsyncMock( side_effect=BulbException if cannot_connect else None @@ -98,7 +106,6 @@ def _mocked_bulb(cannot_connect=False): bulb.last_properties = PROPERTIES.copy() bulb.music_mode = False bulb.async_get_properties = AsyncMock() - bulb.async_listen = AsyncMock() bulb.async_stop_listening = AsyncMock() bulb.async_update = AsyncMock() bulb.async_turn_on = AsyncMock() @@ -116,12 +123,43 @@ def _mocked_bulb(cannot_connect=False): return bulb -def _patch_discovery(prefix, no_device=False): +def _patched_ssdp_listener(info, *args, **kwargs): + listener = SSDPListener(*args, **kwargs) + + async def _async_callback(*_): + await listener.async_connect_callback() + + @callback + def _async_search(*_): + if info: + asyncio.create_task(listener.async_callback(info)) + + listener.async_start = _async_callback + listener.async_search = _async_search + return listener + + +def _patch_discovery(no_device=False): YeelightScanner._scanner = None # Clear class scanner to reset hass - def _mocked_discovery(timeout=2, interface=False): - if no_device: - return [] - return [{"ip": IP_ADDRESS, "port": 55443, "capabilities": CAPABILITIES}] + def _generate_fake_ssdp_listener(*args, **kwargs): + return _patched_ssdp_listener( + None if no_device else CAPABILITIES, + *args, + **kwargs, + ) - return patch(f"{prefix}.discover_bulbs", side_effect=_mocked_discovery) + return patch( + "homeassistant.components.yeelight.SSDPListener", + new=_generate_fake_ssdp_listener, + ) + + +def _patch_discovery_interval(): + return patch.object( + hass_yeelight, "DISCOVERY_SEARCH_INTERVAL", timedelta(seconds=0) + ) + + +def _patch_discovery_timeout(): + return patch.object(hass_yeelight, "DISCOVERY_TIMEOUT", 0.0001) diff --git a/tests/components/yeelight/test_binary_sensor.py b/tests/components/yeelight/test_binary_sensor.py index 472d8de4919..350c289f5b5 100644 --- a/tests/components/yeelight/test_binary_sensor.py +++ b/tests/components/yeelight/test_binary_sensor.py @@ -6,7 +6,14 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_component from homeassistant.setup import async_setup_component -from . import MODULE, NAME, PROPERTIES, YAML_CONFIGURATION, _mocked_bulb +from . import ( + MODULE, + NAME, + PROPERTIES, + YAML_CONFIGURATION, + _mocked_bulb, + _patch_discovery, +) ENTITY_BINARY_SENSOR = f"binary_sensor.{NAME}_nightlight" @@ -14,9 +21,7 @@ ENTITY_BINARY_SENSOR = f"binary_sensor.{NAME}_nightlight" async def test_nightlight(hass: HomeAssistant): """Test nightlight sensor.""" mocked_bulb = _mocked_bulb() - with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), patch( - f"{MODULE}.config_flow.yeelight.Bulb", return_value=mocked_bulb - ): + with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): await async_setup_component(hass, DOMAIN, YAML_CONFIGURATION) await hass.async_block_till_done() diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 247630ecfc3..5bbfcc9283b 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -1,5 +1,5 @@ """Test the Yeelight config flow.""" -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest @@ -25,14 +25,17 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM from . import ( + CAPABILITIES, ID, IP_ADDRESS, MODULE, MODULE_CONFIG_FLOW, NAME, - UNIQUE_NAME, + UNIQUE_FRIENDLY_NAME, _mocked_bulb, _patch_discovery, + _patch_discovery_interval, + _patch_discovery_timeout, ) from tests.common import MockConfigEntry @@ -55,21 +58,23 @@ async def test_discovery(hass: HomeAssistant): assert result["step_id"] == "user" assert not result["errors"] - with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight"): + with _patch_discovery(), _patch_discovery_interval(): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result2["type"] == "form" assert result2["step_id"] == "pick_device" assert not result2["errors"] - with patch(f"{MODULE}.async_setup", return_value=True) as mock_setup, patch( + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE}.async_setup", return_value=True + ) as mock_setup, patch( f"{MODULE}.async_setup_entry", return_value=True ) as mock_setup_entry: result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_DEVICE: ID} ) assert result3["type"] == "create_entry" - assert result3["title"] == UNIQUE_NAME - assert result3["data"] == {CONF_ID: ID} + assert result3["title"] == UNIQUE_FRIENDLY_NAME + assert result3["data"] == {CONF_ID: ID, CONF_HOST: IP_ADDRESS} await hass.async_block_till_done() mock_setup.assert_called_once() mock_setup_entry.assert_called_once() @@ -82,7 +87,7 @@ async def test_discovery(hass: HomeAssistant): assert result["step_id"] == "user" assert not result["errors"] - with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight"): + with _patch_discovery(), _patch_discovery_interval(): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result2["type"] == "abort" assert result2["reason"] == "no_devices_found" @@ -94,7 +99,9 @@ async def test_discovery_no_device(hass: HomeAssistant): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight", no_device=True): + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result2["type"] == "abort" @@ -114,26 +121,27 @@ async def test_import(hass: HomeAssistant): # Cannot connect mocked_bulb = _mocked_bulb(cannot_connect=True) - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config ) - type(mocked_bulb).get_capabilities.assert_called_once() - type(mocked_bulb).get_properties.assert_called_once() assert result["type"] == "abort" assert result["reason"] == "cannot_connect" # Success mocked_bulb = _mocked_bulb() - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch( - f"{MODULE}.async_setup", return_value=True - ) as mock_setup, patch( + with _patch_discovery(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ), 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 ) - type(mocked_bulb).get_capabilities.assert_called_once() assert result["type"] == "create_entry" assert result["title"] == DEFAULT_NAME assert result["data"] == { @@ -150,7 +158,9 @@ async def test_import(hass: HomeAssistant): # Duplicate mocked_bulb = _mocked_bulb() - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + with _patch_discovery(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config ) @@ -169,7 +179,11 @@ async def test_manual(hass: HomeAssistant): # Cannot connect (timeout) mocked_bulb = _mocked_bulb(cannot_connect=True) - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} ) @@ -178,8 +192,11 @@ async def test_manual(hass: HomeAssistant): assert result2["errors"] == {"base": "cannot_connect"} # Cannot connect (error) - type(mocked_bulb).get_capabilities = MagicMock(side_effect=OSError) - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} ) @@ -187,9 +204,11 @@ async def test_manual(hass: HomeAssistant): # Success mocked_bulb = _mocked_bulb() - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch( - f"{MODULE}.async_setup", return_value=True - ), patch(f"{MODULE}.async_setup_entry", return_value=True): + with _patch_discovery(), _patch_discovery_timeout(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ), patch(f"{MODULE}.async_setup", return_value=True), patch( + f"{MODULE}.async_setup_entry", return_value=True + ): result4 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} ) @@ -203,7 +222,11 @@ async def test_manual(hass: HomeAssistant): DOMAIN, context={"source": config_entries.SOURCE_USER} ) mocked_bulb = _mocked_bulb() - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} ) @@ -219,7 +242,7 @@ async def test_options(hass: HomeAssistant): config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): + with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -241,7 +264,7 @@ async def test_options(hass: HomeAssistant): config[CONF_NIGHTLIGHT_SWITCH] = True user_input = {**config} user_input.pop(CONF_NAME) - with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): + with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input ) @@ -262,15 +285,18 @@ async def test_manual_no_capabilities(hass: HomeAssistant): assert not result["errors"] mocked_bulb = _mocked_bulb() - type(mocked_bulb).get_capabilities = MagicMock(return_value=None) - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch( + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ), patch( f"{MODULE}.async_setup", return_value=True - ), patch(f"{MODULE}.async_setup_entry", return_value=True): + ), patch( + f"{MODULE}.async_setup_entry", return_value=True + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} ) - type(mocked_bulb).get_capabilities.assert_called_once() - type(mocked_bulb).get_properties.assert_called_once() assert result["type"] == "create_entry" assert result["data"] == {CONF_HOST: IP_ADDRESS} @@ -280,39 +306,53 @@ async def test_discovered_by_homekit_and_dhcp(hass): await setup.async_setup_component(hass, "persistent_notification", {}) mocked_bulb = _mocked_bulb() - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data={"host": "1.2.3.4", "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + data={"host": IP_ADDRESS, "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, ) + await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_FORM assert result["errors"] is None - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={"ip": "1.2.3.4", "macaddress": "aa:bb:cc:dd:ee:ff"}, + data={"ip": IP_ADDRESS, "macaddress": "aa:bb:cc:dd:ee:ff"}, ) + await hass.async_block_till_done() assert result2["type"] == RESULT_TYPE_ABORT assert result2["reason"] == "already_in_progress" - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data={"ip": "1.2.3.4", "macaddress": "00:00:00:00:00:00"}, + data={"ip": IP_ADDRESS, "macaddress": "00:00:00:00:00:00"}, ) + await hass.async_block_till_done() assert result3["type"] == RESULT_TYPE_ABORT assert result3["reason"] == "already_in_progress" - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", side_effect=CannotConnect): + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", side_effect=CannotConnect + ): result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data={"ip": "1.2.3.5", "macaddress": "00:00:00:00:00:01"}, ) + await hass.async_block_till_done() assert result3["type"] == RESULT_TYPE_ABORT assert result3["reason"] == "cannot_connect" @@ -335,17 +375,25 @@ async def test_discovered_by_dhcp_or_homekit(hass, source, data): await setup.async_setup_component(hass, "persistent_notification", {}) mocked_bulb = _mocked_bulb() - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): 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_FORM assert result["errors"] is None - with patch(f"{MODULE}.async_setup", return_value=True) as mock_async_setup, patch( + with _patch_discovery(), _patch_discovery_interval(), 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_ID: "0x000000000015243f"} assert mock_async_setup.called @@ -370,10 +418,55 @@ async def test_discovered_by_dhcp_or_homekit_failed_to_get_id(hass, source, data await setup.async_setup_component(hass, "persistent_notification", {}) mocked_bulb = _mocked_bulb() - type(mocked_bulb).get_capabilities = MagicMock(return_value=None) - with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, data=data ) assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "cannot_connect" + + +async def test_discovered_ssdp(hass): + """Test we can setup when discovered from ssdp.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mocked_bulb = _mocked_bulb() + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=CAPABILITIES + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with _patch_discovery(), _patch_discovery_interval(), 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_ID: "0x000000000015243f"} + assert mock_async_setup.called + assert mock_async_setup_entry.called + + mocked_bulb = _mocked_bulb() + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=CAPABILITIES + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index 575ad4cb594..d7f4a05b436 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -1,5 +1,6 @@ """Test Yeelight.""" -from unittest.mock import AsyncMock, MagicMock, patch +from datetime import timedelta +from unittest.mock import AsyncMock, patch from yeelight import BulbException, BulbType @@ -22,9 +23,9 @@ from homeassistant.const import ( 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 from . import ( - CAPABILITIES, CONFIG_ENTRY_DATA, ENTITY_AMBILIGHT, ENTITY_BINARY_SENSOR, @@ -34,12 +35,14 @@ from . import ( ID, IP_ADDRESS, MODULE, - MODULE_CONFIG_FLOW, + SHORT_ID, _mocked_bulb, _patch_discovery, + _patch_discovery_interval, + _patch_discovery_timeout, ) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_ip_changes_fallback_discovery(hass: HomeAssistant): @@ -51,19 +54,15 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant): mocked_bulb = _mocked_bulb(True) mocked_bulb.bulb_type = BulbType.WhiteTempMood - mocked_bulb.get_capabilities = MagicMock( - side_effect=[OSError, CAPABILITIES, CAPABILITIES] - ) + mocked_bulb.async_listen = AsyncMock(side_effect=[BulbException, None, None, None]) - _discovered_devices = [{"capabilities": CAPABILITIES, "ip": IP_ADDRESS}] - with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), patch( - f"{MODULE}.discover_bulbs", return_value=_discovered_devices - ): + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery(): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + await hass.async_block_till_done() binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format( - f"yeelight_color_{ID}" + f"yeelight_color_{SHORT_ID}" ) type(mocked_bulb).async_get_properties = AsyncMock(None) @@ -77,6 +76,19 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant): entity_registry = er.async_get(hass) assert entity_registry.async_get(binary_sensor_entity_id) is not None + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery(): + # The discovery should update the ip address + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2)) + await hass.async_block_till_done() + assert config_entry.data[CONF_HOST] == IP_ADDRESS + + # Make sure we can still reload with the new ip right after we change it + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery(): + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + assert entity_registry.async_get(binary_sensor_entity_id) is not None + async def test_ip_changes_id_missing_cannot_fallback(hass: HomeAssistant): """Test Yeelight ip changes and we fallback to discovery.""" @@ -85,9 +97,7 @@ async def test_ip_changes_id_missing_cannot_fallback(hass: HomeAssistant): mocked_bulb = _mocked_bulb(True) mocked_bulb.bulb_type = BulbType.WhiteTempMood - mocked_bulb.get_capabilities = MagicMock( - side_effect=[OSError, CAPABILITIES, CAPABILITIES] - ) + mocked_bulb.async_listen = AsyncMock(side_effect=[BulbException, None, None, None]) with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): assert not await hass.config_entries.async_setup(config_entry.entry_id) @@ -102,9 +112,7 @@ async def test_setup_discovery(hass: HomeAssistant): config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with _patch_discovery(MODULE), patch( - f"{MODULE}.AsyncBulb", return_value=mocked_bulb - ): + with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -127,9 +135,7 @@ async def test_setup_import(hass: HomeAssistant): """Test import from yaml.""" mocked_bulb = _mocked_bulb() name = "yeelight" - with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), patch( - f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb - ): + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery(): assert await async_setup_component( hass, DOMAIN, @@ -162,9 +168,7 @@ async def test_unique_ids_device(hass: HomeAssistant): mocked_bulb = _mocked_bulb() mocked_bulb.bulb_type = BulbType.WhiteTempMood - with _patch_discovery(MODULE), patch( - f"{MODULE}.AsyncBulb", return_value=mocked_bulb - ): + with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -188,9 +192,7 @@ async def test_unique_ids_entry(hass: HomeAssistant): mocked_bulb = _mocked_bulb() mocked_bulb.bulb_type = BulbType.WhiteTempMood - with _patch_discovery(MODULE), patch( - f"{MODULE}.AsyncBulb", return_value=mocked_bulb - ): + with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -220,30 +222,13 @@ async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant): mocked_bulb = _mocked_bulb(True) mocked_bulb.bulb_type = BulbType.WhiteTempMood - with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), patch( - f"{MODULE}.config_flow.yeelight.Bulb", return_value=mocked_bulb - ): + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format( - IP_ADDRESS.replace(".", "_") - ) - - type(mocked_bulb).get_capabilities = MagicMock(CAPABILITIES) - type(mocked_bulb).get_properties = MagicMock(None) - - await hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][ - DATA_DEVICE - ].async_update() - hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][ - DATA_DEVICE - ].async_update_callback({}) - await hass.async_block_till_done() - await hass.async_block_till_done() - - entity_registry = er.async_get(hass) - assert entity_registry.async_get(binary_sensor_entity_id) is not None + assert config_entry.state is ConfigEntryState.LOADED async def test_async_listen_error_late_discovery(hass, caplog): @@ -251,12 +236,9 @@ async def test_async_listen_error_late_discovery(hass, caplog): config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) config_entry.add_to_hass(hass) - mocked_bulb = _mocked_bulb() - mocked_bulb.async_listen = AsyncMock(side_effect=BulbException) + mocked_bulb = _mocked_bulb(cannot_connect=True) - with _patch_discovery(MODULE), patch( - f"{MODULE}.AsyncBulb", return_value=mocked_bulb - ): + with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -264,17 +246,33 @@ async def test_async_listen_error_late_discovery(hass, caplog): assert "Failed to connect to bulb at" in caplog.text -async def test_async_listen_error_has_host(hass: HomeAssistant): +async def test_async_listen_error_has_host_with_id(hass: HomeAssistant): """Test the async listen error.""" config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_ID: ID, CONF_HOST: "127.0.0.1"} ) config_entry.add_to_hass(hass) - mocked_bulb = _mocked_bulb() - mocked_bulb.async_listen = AsyncMock(side_effect=BulbException) + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE}.AsyncBulb", return_value=_mocked_bulb(cannot_connect=True) + ): + await hass.config_entries.async_setup(config_entry.entry_id) - with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): + assert config_entry.state is ConfigEntryState.LOADED + + +async def test_async_listen_error_has_host_without_id(hass: HomeAssistant): + """Test the async listen error but no id.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}) + 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(cannot_connect=True) + ): await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index 8b7ec154b83..7497fa8773e 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -102,9 +102,10 @@ from . import ( MODULE, NAME, PROPERTIES, - UNIQUE_NAME, + UNIQUE_FRIENDLY_NAME, _mocked_bulb, _patch_discovery, + _patch_discovery_interval, ) from tests.common import MockConfigEntry @@ -132,7 +133,7 @@ async def test_services(hass: HomeAssistant, caplog): config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with _patch_discovery(MODULE), patch( + with _patch_discovery(), _patch_discovery_interval(), patch( f"{MODULE}.AsyncBulb", return_value=mocked_bulb ): assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -559,7 +560,7 @@ async def test_device_types(hass: HomeAssistant, caplog): model, target_properties, nightlight_properties=None, - name=UNIQUE_NAME, + name=UNIQUE_FRIENDLY_NAME, entity_id=ENTITY_LIGHT, ): config_entry = MockConfigEntry( @@ -598,7 +599,7 @@ async def test_device_types(hass: HomeAssistant, caplog): assert hass.states.get(entity_id).state == "off" state = hass.states.get(f"{entity_id}_nightlight") assert state.state == "on" - nightlight_properties["friendly_name"] = f"{name} nightlight" + nightlight_properties["friendly_name"] = f"{name} Nightlight" nightlight_properties["icon"] = "mdi:weather-night" nightlight_properties["flowing"] = False nightlight_properties["night_light"] = True @@ -893,7 +894,7 @@ async def test_device_types(hass: HomeAssistant, caplog): "color_mode": "color_temp", "supported_color_modes": ["color_temp", "hs", "rgb"], }, - name=f"{UNIQUE_NAME} ambilight", + name=f"{UNIQUE_FRIENDLY_NAME} Ambilight", entity_id=f"{ENTITY_LIGHT}_ambilight", ) @@ -914,7 +915,7 @@ async def test_device_types(hass: HomeAssistant, caplog): "color_mode": "hs", "supported_color_modes": ["color_temp", "hs", "rgb"], }, - name=f"{UNIQUE_NAME} ambilight", + name=f"{UNIQUE_FRIENDLY_NAME} Ambilight", entity_id=f"{ENTITY_LIGHT}_ambilight", ) @@ -935,7 +936,7 @@ async def test_device_types(hass: HomeAssistant, caplog): "color_mode": "rgb", "supported_color_modes": ["color_temp", "hs", "rgb"], }, - name=f"{UNIQUE_NAME} ambilight", + name=f"{UNIQUE_FRIENDLY_NAME} Ambilight", entity_id=f"{ENTITY_LIGHT}_ambilight", ) @@ -969,7 +970,7 @@ async def test_effects(hass: HomeAssistant): config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with _patch_discovery(MODULE), patch( + with _patch_discovery(), _patch_discovery_interval(), patch( f"{MODULE}.AsyncBulb", return_value=mocked_bulb ): assert await hass.config_entries.async_setup(config_entry.entry_id) From 08193169d0da03c81635ab05235cc9e94ec18abb Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 18 Aug 2021 13:27:41 -0400 Subject: [PATCH 338/355] Remove unnecessary signal during zwave_js.reset_meter service call (#54837) --- homeassistant/components/zwave_js/sensor.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 220184f5669..1ffa263dae7 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -31,10 +31,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_METER_TYPE, ATTR_VALUE, DATA_CLIENT, DOMAIN, SERVICE_RESET_METER @@ -256,15 +253,6 @@ class ZWaveMeterSensor(ZWaveNumericSensor): options, ) - # Notify meters that may have been reset - async_dispatcher_send( - self.hass, - f"{DOMAIN}_{SERVICE_RESET_METER}", - node, - primary_value.endpoint, - options.get("type"), - ) - class ZWaveListSensor(ZwaveSensorBase): """Representation of a Z-Wave Numeric sensor with multiple states.""" From 30564d59b67b860bf734a97d21b0c45b9731171a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Aug 2021 12:32:52 -0500 Subject: [PATCH 339/355] Bump yeelight quality scale to platinum with switch to async local push (#54589) --- homeassistant/components/yeelight/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 4c5994b1f6e..b1c1c131907 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -5,6 +5,7 @@ "requirements": ["yeelight==0.7.2", "async-upnp-client==0.20.0"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"], "config_flow": true, + "quality_scale": "platinum", "iot_class": "local_push", "dhcp": [{ "hostname": "yeelink-*" From 6d0ce814e79b72b13e8f2eff371e926ddba155ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Aug 2021 12:33:26 -0500 Subject: [PATCH 340/355] Add new network apis to reduce code duplication (#54832) --- homeassistant/components/network/__init__.py | 32 +++++++++++++++- homeassistant/components/ssdp/__init__.py | 30 ++++----------- homeassistant/components/zeroconf/__init__.py | 37 +++++-------------- 3 files changed, 48 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/network/__init__.py b/homeassistant/components/network/__init__.py index 48903d145e7..a7dffad7084 100644 --- a/homeassistant/components/network/__init__.py +++ b/homeassistant/components/network/__init__.py @@ -1,13 +1,14 @@ """The Network Configuration integration.""" from __future__ import annotations +from ipaddress import IPv4Address, IPv6Address import logging import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.websocket_api.connection import ActiveConnection -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass @@ -45,6 +46,35 @@ async def async_get_source_ip(hass: HomeAssistant, target_ip: str) -> str: return source_ip if source_ip in all_ipv4s else all_ipv4s[0] +@bind_hass +async def async_get_enabled_source_ips( + hass: HomeAssistant, +) -> list[IPv4Address | IPv6Address]: + """Build the list of enabled source ips.""" + adapters = await async_get_adapters(hass) + sources: list[IPv4Address | IPv6Address] = [] + for adapter in adapters: + if not adapter["enabled"]: + continue + if adapter["ipv4"]: + sources.extend(IPv4Address(ipv4["address"]) for ipv4 in adapter["ipv4"]) + if adapter["ipv6"]: + # With python 3.9 add scope_ids can be + # added by enumerating adapter["ipv6"]s + # IPv6Address(f"::%{ipv6['scope_id']}") + sources.extend(IPv6Address(ipv6["address"]) for ipv6 in adapter["ipv6"]) + + return sources + + +@callback +def async_only_default_interface_enabled(adapters: list[Adapter]) -> bool: + """Check to see if any non-default adapter is enabled.""" + return not any( + adapter["enabled"] and not adapter["default"] for adapter in adapters + ) + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up network for Home Assistant.""" diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 4d21fdb6aab..1fd2bba77cc 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -116,14 +116,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -@core_callback -def _async_use_default_interface(adapters: list[network.Adapter]) -> bool: - for adapter in adapters: - if adapter["enabled"] and not adapter["default"]: - return False - return True - - @core_callback def _async_process_callbacks( callbacks: list[Callable[[dict], None]], discovery_info: dict[str, str] @@ -204,24 +196,16 @@ class Scanner: """Build the list of ssdp sources.""" adapters = await network.async_get_adapters(self.hass) sources: set[IPv4Address | IPv6Address] = set() - if _async_use_default_interface(adapters): + if network.async_only_default_interface_enabled(adapters): sources.add(IPv4Address("0.0.0.0")) return sources - for adapter in adapters: - if not adapter["enabled"]: - continue - if adapter["ipv4"]: - ipv4 = adapter["ipv4"][0] - sources.add(IPv4Address(ipv4["address"])) - if adapter["ipv6"]: - ipv6 = adapter["ipv6"][0] - # With python 3.9 add scope_ids can be - # added by enumerating adapter["ipv6"]s - # IPv6Address(f"::%{ipv6['scope_id']}") - sources.add(IPv6Address(ipv6["address"])) - - return sources + return { + source_ip + for source_ip in await network.async_get_enabled_source_ips(self.hass) + if not source_ip.is_loopback + and not (isinstance(source_ip, IPv6Address) and source_ip.is_global) + } async def async_scan(self, *_: Any) -> None: """Scan for new entries using ssdp default and broadcast target.""" diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 6829c9c5e17..8b1f482e05e 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -5,7 +5,7 @@ import asyncio from collections.abc import Coroutine from contextlib import suppress import fnmatch -import ipaddress +from ipaddress import IPv6Address, ip_address import logging import socket from typing import Any, TypedDict, cast @@ -131,13 +131,6 @@ async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaAsyncZero return aio_zc -def _async_use_default_interface(adapters: list[Adapter]) -> bool: - for adapter in adapters: - if adapter["enabled"] and not adapter["default"]: - return False - return True - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Zeroconf and make Home Assistant discoverable.""" zc_args: dict = {} @@ -151,25 +144,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: else: zc_args["ip_version"] = IPVersion.All - if not ipv6 and _async_use_default_interface(adapters): + if not ipv6 and network.async_only_default_interface_enabled(adapters): zc_args["interfaces"] = InterfaceChoice.Default else: - interfaces = zc_args["interfaces"] = [] - for adapter in adapters: - if not adapter["enabled"]: - continue - if ipv4s := adapter["ipv4"]: - interfaces.extend( - ipv4["address"] - for ipv4 in ipv4s - if not ipaddress.IPv4Address(ipv4["address"]).is_loopback - ) - if ipv6s := adapter["ipv6"]: - for ipv6_addr in ipv6s: - address = ipv6_addr["address"] - v6_ip_address = ipaddress.IPv6Address(address) - if not v6_ip_address.is_global and not v6_ip_address.is_loopback: - interfaces.append(ipv6_addr["address"]) + zc_args["interfaces"] = [ + str(source_ip) + for source_ip in await network.async_get_enabled_source_ips(hass) + if not source_ip.is_loopback + and not (isinstance(source_ip, IPv6Address) and source_ip.is_global) + ] aio_zc = await _async_get_instance(hass, **zc_args) zeroconf = cast(HaZeroconf, aio_zc.zeroconf) @@ -213,7 +196,7 @@ def _get_announced_addresses( addresses = { addr.packed for addr in [ - ipaddress.ip_address(ip["address"]) + ip_address(ip["address"]) for adapter in adapters if adapter["enabled"] for ip in cast(list, adapter["ipv6"]) + cast(list, adapter["ipv4"]) @@ -530,7 +513,7 @@ def info_from_service(service: AsyncServiceInfo) -> HaServiceInfo | None: address = service.addresses[0] return { - "host": str(ipaddress.ip_address(address)), + "host": str(ip_address(address)), "port": service.port, "hostname": service.server, "type": service.type, From 2f77b5025ca565d6ea174fe4f29c5a68343ba1cf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 Aug 2021 11:21:51 -0700 Subject: [PATCH 341/355] Add energy validation (#54567) --- homeassistant/components/energy/manifest.json | 2 +- homeassistant/components/energy/validate.py | 277 +++++++++++ .../components/energy/websocket_api.py | 17 + homeassistant/components/recorder/__init__.py | 11 + tests/components/energy/test_validate.py | 443 ++++++++++++++++++ tests/components/energy/test_websocket_api.py | 16 + 6 files changed, 765 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/energy/validate.py create mode 100644 tests/components/energy/test_validate.py diff --git a/homeassistant/components/energy/manifest.json b/homeassistant/components/energy/manifest.json index 3a3cbeff4e7..5ddc6457a61 100644 --- a/homeassistant/components/energy/manifest.json +++ b/homeassistant/components/energy/manifest.json @@ -4,6 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/energy", "codeowners": ["@home-assistant/core"], "iot_class": "calculated", - "dependencies": ["websocket_api", "history"], + "dependencies": ["websocket_api", "history", "recorder"], "quality_scale": "internal" } diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py new file mode 100644 index 00000000000..01709081d68 --- /dev/null +++ b/homeassistant/components/energy/validate.py @@ -0,0 +1,277 @@ +"""Validate the energy preferences provide valid data.""" +from __future__ import annotations + +import dataclasses +from typing import Any + +from homeassistant.components import recorder, sensor +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR, + ENERGY_WATT_HOUR, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant, callback, valid_entity_id + +from . import data +from .const import DOMAIN + + +@dataclasses.dataclass +class ValidationIssue: + """Error or warning message.""" + + type: str + identifier: str + value: Any | None = None + + +@dataclasses.dataclass +class EnergyPreferencesValidation: + """Dictionary holding validation information.""" + + energy_sources: list[list[ValidationIssue]] = dataclasses.field( + default_factory=list + ) + device_consumption: list[list[ValidationIssue]] = dataclasses.field( + default_factory=list + ) + + def as_dict(self) -> dict: + """Return dictionary version.""" + return dataclasses.asdict(self) + + +@callback +def _async_validate_energy_stat( + hass: HomeAssistant, stat_value: str, result: list[ValidationIssue] +) -> None: + """Validate a statistic.""" + has_entity_source = valid_entity_id(stat_value) + + if not has_entity_source: + return + + if not recorder.is_entity_recorded(hass, stat_value): + result.append( + ValidationIssue( + "recorder_untracked", + stat_value, + ) + ) + return + + state = hass.states.get(stat_value) + + if state is None: + result.append( + ValidationIssue( + "entity_not_defined", + stat_value, + ) + ) + return + + if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): + result.append(ValidationIssue("entity_unavailable", stat_value, state.state)) + return + + try: + current_value: float | None = float(state.state) + except ValueError: + result.append( + ValidationIssue("entity_state_non_numeric", stat_value, state.state) + ) + return + + if current_value is not None and current_value < 0: + result.append( + ValidationIssue("entity_negative_state", stat_value, current_value) + ) + + unit = state.attributes.get("unit_of_measurement") + + if unit not in (ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR): + result.append( + ValidationIssue("entity_unexpected_unit_energy", stat_value, unit) + ) + + state_class = state.attributes.get("state_class") + + if state_class != sensor.STATE_CLASS_TOTAL_INCREASING: + result.append( + ValidationIssue( + "entity_unexpected_state_class_total_increasing", + stat_value, + state_class, + ) + ) + + +@callback +def _async_validate_price_entity( + hass: HomeAssistant, entity_id: str, result: list[ValidationIssue] +) -> None: + """Validate that the price entity is correct.""" + state = hass.states.get(entity_id) + + if state is None: + result.append( + ValidationIssue( + "entity_not_defined", + entity_id, + ) + ) + return + + try: + value: float | None = float(state.state) + except ValueError: + result.append( + ValidationIssue("entity_state_non_numeric", entity_id, state.state) + ) + return + + if value is not None and value < 0: + result.append(ValidationIssue("entity_negative_state", entity_id, value)) + + unit = state.attributes.get("unit_of_measurement") + + if unit is None or not unit.endswith( + (f"/{ENERGY_KILO_WATT_HOUR}", f"/{ENERGY_WATT_HOUR}") + ): + result.append(ValidationIssue("entity_unexpected_unit_price", entity_id, unit)) + + +@callback +def _async_validate_cost_stat( + hass: HomeAssistant, stat_id: str, result: list[ValidationIssue] +) -> None: + """Validate that the cost stat is correct.""" + has_entity = valid_entity_id(stat_id) + + if not has_entity: + return + + if not recorder.is_entity_recorded(hass, stat_id): + result.append( + ValidationIssue( + "recorder_untracked", + stat_id, + ) + ) + + +@callback +def _async_validate_cost_entity( + hass: HomeAssistant, entity_id: str, result: list[ValidationIssue] +) -> None: + """Validate that the cost entity is correct.""" + if not recorder.is_entity_recorded(hass, entity_id): + result.append( + ValidationIssue( + "recorder_untracked", + entity_id, + ) + ) + + state = hass.states.get(entity_id) + + if state is None: + result.append( + ValidationIssue( + "entity_not_defined", + entity_id, + ) + ) + return + + state_class = state.attributes.get("state_class") + + if state_class != sensor.STATE_CLASS_TOTAL_INCREASING: + result.append( + ValidationIssue( + "entity_unexpected_state_class_total_increasing", entity_id, state_class + ) + ) + + +async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: + """Validate the energy configuration.""" + manager = await data.async_get_manager(hass) + + result = EnergyPreferencesValidation() + + if manager.data is None: + return result + + for source in manager.data["energy_sources"]: + source_result: list[ValidationIssue] = [] + result.energy_sources.append(source_result) + + if source["type"] == "grid": + for flow in source["flow_from"]: + _async_validate_energy_stat( + hass, flow["stat_energy_from"], source_result + ) + + if flow.get("stat_cost") is not None: + _async_validate_cost_stat(hass, flow["stat_cost"], source_result) + + elif flow.get("entity_energy_price") is not None: + _async_validate_price_entity( + hass, flow["entity_energy_price"], source_result + ) + _async_validate_cost_entity( + hass, + hass.data[DOMAIN]["cost_sensors"][flow["stat_energy_from"]], + source_result, + ) + + for flow in source["flow_to"]: + _async_validate_energy_stat(hass, flow["stat_energy_to"], source_result) + + if flow.get("stat_compensation") is not None: + _async_validate_cost_stat( + hass, flow["stat_compensation"], source_result + ) + + elif flow.get("entity_energy_price") is not None: + _async_validate_price_entity( + hass, flow["entity_energy_price"], source_result + ) + _async_validate_cost_entity( + hass, + hass.data[DOMAIN]["cost_sensors"][flow["stat_energy_to"]], + source_result, + ) + + elif source["type"] == "gas": + _async_validate_energy_stat(hass, source["stat_energy_from"], source_result) + + if source.get("stat_cost") is not None: + _async_validate_cost_stat(hass, source["stat_cost"], source_result) + + elif source.get("entity_energy_price") is not None: + _async_validate_price_entity( + hass, source["entity_energy_price"], source_result + ) + _async_validate_cost_entity( + hass, + hass.data[DOMAIN]["cost_sensors"][source["stat_energy_from"]], + source_result, + ) + + elif source["type"] == "solar": + _async_validate_energy_stat(hass, source["stat_energy_from"], source_result) + + elif source["type"] == "battery": + _async_validate_energy_stat(hass, source["stat_energy_from"], source_result) + _async_validate_energy_stat(hass, source["stat_energy_to"], source_result) + + for device in manager.data["device_consumption"]: + device_result: list[ValidationIssue] = [] + result.device_consumption.append(device_result) + _async_validate_energy_stat(hass, device["stat_consumption"], device_result) + + return result diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py index d1c8869a1c2..6d71a75b9b4 100644 --- a/homeassistant/components/energy/websocket_api.py +++ b/homeassistant/components/energy/websocket_api.py @@ -18,6 +18,7 @@ from .data import ( EnergyPreferencesUpdate, async_get_manager, ) +from .validate import async_validate EnergyWebSocketCommandHandler = Callable[ [HomeAssistant, websocket_api.ActiveConnection, Dict[str, Any], "EnergyManager"], @@ -35,6 +36,7 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_get_prefs) websocket_api.async_register_command(hass, ws_save_prefs) websocket_api.async_register_command(hass, ws_info) + websocket_api.async_register_command(hass, ws_validate) def _ws_with_manager( @@ -113,3 +115,18 @@ def ws_info( ) -> None: """Handle get info command.""" connection.send_result(msg["id"], hass.data[DOMAIN]) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "energy/validate", + } +) +@websocket_api.async_response +async def ws_validate( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, +) -> None: + """Handle validate command.""" + connection.send_result(msg["id"], (await async_validate(hass)).as_dict()) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index e9c12e5f88a..e6c15729d24 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -174,6 +174,17 @@ async def async_migration_in_progress(hass: HomeAssistant) -> bool: return hass.data[DATA_INSTANCE].migration_in_progress +@bind_hass +def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool: + """Check if an entity is being recorded. + + Async friendly. + """ + if DATA_INSTANCE not in hass.data: + return False + return hass.data[DATA_INSTANCE].entity_filter(entity_id) + + def run_information(hass, point_in_time: datetime | None = None): """Return information about current run. diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py new file mode 100644 index 00000000000..9a0b2105007 --- /dev/null +++ b/tests/components/energy/test_validate.py @@ -0,0 +1,443 @@ +"""Test that validation works.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.energy import async_get_manager, validate +from homeassistant.setup import async_setup_component + +from tests.common import async_init_recorder_component + + +@pytest.fixture +def mock_is_entity_recorded(): + """Mock recorder.is_entity_recorded.""" + mocks = {} + + with patch( + "homeassistant.components.recorder.is_entity_recorded", + side_effect=lambda hass, entity_id: mocks.get(entity_id, True), + ): + yield mocks + + +@pytest.fixture(autouse=True) +async def mock_energy_manager(hass): + """Set up energy.""" + await async_init_recorder_component(hass) + assert await async_setup_component(hass, "energy", {"energy": {}}) + manager = await async_get_manager(hass) + manager.data = manager.default_preferences() + return manager + + +async def test_validation_empty_config(hass): + """Test validating an empty config.""" + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [], + "device_consumption": [], + } + + +async def test_validation(hass, mock_energy_manager): + """Test validating success.""" + for key in ("device_cons", "battery_import", "battery_export", "solar_production"): + hass.states.async_set( + f"sensor.{key}", + "123", + {"unit_of_measurement": "kWh", "state_class": "total_increasing"}, + ) + + await mock_energy_manager.async_update( + { + "energy_sources": [ + { + "type": "battery", + "stat_energy_from": "sensor.battery_import", + "stat_energy_to": "sensor.battery_export", + }, + {"type": "solar", "stat_energy_from": "sensor.solar_production"}, + ], + "device_consumption": [{"stat_consumption": "sensor.device_cons"}], + } + ) + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [[], []], + "device_consumption": [[]], + } + + +async def test_validation_device_consumption_entity_missing(hass, mock_energy_manager): + """Test validating missing stat for device.""" + await mock_energy_manager.async_update( + {"device_consumption": [{"stat_consumption": "sensor.not_exist"}]} + ) + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [], + "device_consumption": [ + [ + { + "type": "entity_not_defined", + "identifier": "sensor.not_exist", + "value": None, + } + ] + ], + } + + +async def test_validation_device_consumption_entity_unavailable( + hass, mock_energy_manager +): + """Test validating missing stat for device.""" + await mock_energy_manager.async_update( + {"device_consumption": [{"stat_consumption": "sensor.unavailable"}]} + ) + hass.states.async_set("sensor.unavailable", "unavailable", {}) + + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [], + "device_consumption": [ + [ + { + "type": "entity_unavailable", + "identifier": "sensor.unavailable", + "value": "unavailable", + } + ] + ], + } + + +async def test_validation_device_consumption_entity_non_numeric( + hass, mock_energy_manager +): + """Test validating missing stat for device.""" + await mock_energy_manager.async_update( + {"device_consumption": [{"stat_consumption": "sensor.non_numeric"}]} + ) + hass.states.async_set("sensor.non_numeric", "123,123.10") + + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [], + "device_consumption": [ + [ + { + "type": "entity_state_non_numeric", + "identifier": "sensor.non_numeric", + "value": "123,123.10", + }, + ] + ], + } + + +async def test_validation_device_consumption_entity_unexpected_unit( + hass, mock_energy_manager +): + """Test validating missing stat for device.""" + await mock_energy_manager.async_update( + {"device_consumption": [{"stat_consumption": "sensor.unexpected_unit"}]} + ) + hass.states.async_set( + "sensor.unexpected_unit", + "10.10", + {"unit_of_measurement": "beers", "state_class": "total_increasing"}, + ) + + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [], + "device_consumption": [ + [ + { + "type": "entity_unexpected_unit_energy", + "identifier": "sensor.unexpected_unit", + "value": "beers", + } + ] + ], + } + + +async def test_validation_device_consumption_recorder_not_tracked( + hass, mock_energy_manager, mock_is_entity_recorded +): + """Test validating device based on untracked entity.""" + mock_is_entity_recorded["sensor.not_recorded"] = False + await mock_energy_manager.async_update( + {"device_consumption": [{"stat_consumption": "sensor.not_recorded"}]} + ) + + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [], + "device_consumption": [ + [ + { + "type": "recorder_untracked", + "identifier": "sensor.not_recorded", + "value": None, + } + ] + ], + } + + +async def test_validation_solar(hass, mock_energy_manager): + """Test validating missing stat for device.""" + await mock_energy_manager.async_update( + { + "energy_sources": [ + {"type": "solar", "stat_energy_from": "sensor.solar_production"} + ] + } + ) + hass.states.async_set( + "sensor.solar_production", + "10.10", + {"unit_of_measurement": "beers", "state_class": "total_increasing"}, + ) + + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [ + [ + { + "type": "entity_unexpected_unit_energy", + "identifier": "sensor.solar_production", + "value": "beers", + } + ] + ], + "device_consumption": [], + } + + +async def test_validation_battery(hass, mock_energy_manager): + """Test validating missing stat for device.""" + await mock_energy_manager.async_update( + { + "energy_sources": [ + { + "type": "battery", + "stat_energy_from": "sensor.battery_import", + "stat_energy_to": "sensor.battery_export", + } + ] + } + ) + hass.states.async_set( + "sensor.battery_import", + "10.10", + {"unit_of_measurement": "beers", "state_class": "total_increasing"}, + ) + hass.states.async_set( + "sensor.battery_export", + "10.10", + {"unit_of_measurement": "beers", "state_class": "total_increasing"}, + ) + + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [ + [ + { + "type": "entity_unexpected_unit_energy", + "identifier": "sensor.battery_import", + "value": "beers", + }, + { + "type": "entity_unexpected_unit_energy", + "identifier": "sensor.battery_export", + "value": "beers", + }, + ] + ], + "device_consumption": [], + } + + +async def test_validation_grid(hass, mock_energy_manager, mock_is_entity_recorded): + """Test validating grid with sensors for energy and cost/compensation.""" + mock_is_entity_recorded["sensor.grid_cost_1"] = False + mock_is_entity_recorded["sensor.grid_compensation_1"] = False + await mock_energy_manager.async_update( + { + "energy_sources": [ + { + "type": "grid", + "flow_from": [ + { + "stat_energy_from": "sensor.grid_consumption_1", + "stat_cost": "sensor.grid_cost_1", + } + ], + "flow_to": [ + { + "stat_energy_to": "sensor.grid_production_1", + "stat_compensation": "sensor.grid_compensation_1", + } + ], + } + ] + } + ) + hass.states.async_set( + "sensor.grid_consumption_1", + "10.10", + {"unit_of_measurement": "beers", "state_class": "total_increasing"}, + ) + hass.states.async_set( + "sensor.grid_production_1", + "10.10", + {"unit_of_measurement": "beers", "state_class": "total_increasing"}, + ) + + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [ + [ + { + "type": "entity_unexpected_unit_energy", + "identifier": "sensor.grid_consumption_1", + "value": "beers", + }, + { + "type": "recorder_untracked", + "identifier": "sensor.grid_cost_1", + "value": None, + }, + { + "type": "entity_unexpected_unit_energy", + "identifier": "sensor.grid_production_1", + "value": "beers", + }, + { + "type": "recorder_untracked", + "identifier": "sensor.grid_compensation_1", + "value": None, + }, + ] + ], + "device_consumption": [], + } + + +async def test_validation_grid_price_not_exist(hass, mock_energy_manager): + """Test validating grid with price entity that does not exist.""" + hass.states.async_set( + "sensor.grid_consumption_1", + "10.10", + {"unit_of_measurement": "kWh", "state_class": "total_increasing"}, + ) + hass.states.async_set( + "sensor.grid_production_1", + "10.10", + {"unit_of_measurement": "kWh", "state_class": "total_increasing"}, + ) + await mock_energy_manager.async_update( + { + "energy_sources": [ + { + "type": "grid", + "flow_from": [ + { + "stat_energy_from": "sensor.grid_consumption_1", + "entity_energy_from": "sensor.grid_consumption_1", + "entity_energy_price": "sensor.grid_price_1", + } + ], + "flow_to": [ + { + "stat_energy_to": "sensor.grid_production_1", + "entity_energy_to": "sensor.grid_production_1", + "number_energy_price": 0.10, + } + ], + } + ] + } + ) + await hass.async_block_till_done() + + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [ + [ + { + "type": "entity_not_defined", + "identifier": "sensor.grid_price_1", + "value": None, + } + ] + ], + "device_consumption": [], + } + + +@pytest.mark.parametrize( + "state, unit, expected", + ( + ( + "123,123.12", + "$/kWh", + { + "type": "entity_state_non_numeric", + "identifier": "sensor.grid_price_1", + "value": "123,123.12", + }, + ), + ( + "-100", + "$/kWh", + { + "type": "entity_negative_state", + "identifier": "sensor.grid_price_1", + "value": -100.0, + }, + ), + ( + "123", + "$/Ws", + { + "type": "entity_unexpected_unit_price", + "identifier": "sensor.grid_price_1", + "value": "$/Ws", + }, + ), + ), +) +async def test_validation_grid_price_errors( + hass, mock_energy_manager, state, unit, expected +): + """Test validating grid with price data that gives errors.""" + hass.states.async_set( + "sensor.grid_consumption_1", + "10.10", + {"unit_of_measurement": "kWh", "state_class": "total_increasing"}, + ) + hass.states.async_set( + "sensor.grid_price_1", + state, + {"unit_of_measurement": unit, "state_class": "total_increasing"}, + ) + await mock_energy_manager.async_update( + { + "energy_sources": [ + { + "type": "grid", + "flow_from": [ + { + "stat_energy_from": "sensor.grid_consumption_1", + "entity_energy_from": "sensor.grid_consumption_1", + "entity_energy_price": "sensor.grid_price_1", + } + ], + "flow_to": [], + } + ] + } + ) + await hass.async_block_till_done() + + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [ + [expected], + ], + "device_consumption": [], + } diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index 60ac82108bc..732bdaa93cf 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -216,3 +216,19 @@ async def test_handle_duplicate_from_stat(hass, hass_ws_client) -> None: assert msg["id"] == 5 assert not msg["success"] assert msg["error"]["code"] == "invalid_format" + + +async def test_validate(hass, hass_ws_client) -> None: + """Test we can validate the preferences.""" + client = await hass_ws_client(hass) + + await client.send_json({"id": 5, "type": "energy/validate"}) + + msg = await client.receive_json() + + assert msg["id"] == 5 + assert msg["success"] + assert msg["result"] == { + "energy_sources": [], + "device_consumption": [], + } From f9fa5fa804291cdc3c2ab9592b3841fb2444bb72 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 20:22:01 +0200 Subject: [PATCH 342/355] Deprecate last_reset options in MQTT sensor (#54840) --- homeassistant/components/mqtt/sensor.py | 29 +++++++++++++++---------- tests/components/mqtt/test_sensor.py | 22 +++++++++++++++++++ 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index eac136d3f84..16c19c8fc51 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -53,18 +53,23 @@ MQTT_SENSOR_ATTRIBUTES_BLOCKED = frozenset( DEFAULT_NAME = "MQTT Sensor" DEFAULT_FORCE_UPDATE = False -PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, - vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, - vol.Optional(CONF_LAST_RESET_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_LAST_RESET_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - } -).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) +PLATFORM_SCHEMA = vol.All( + # Deprecated, remove in Home Assistant 2021.11 + cv.deprecated(CONF_LAST_RESET_TOPIC), + cv.deprecated(CONF_LAST_RESET_VALUE_TEMPLATE), + mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, + vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, + vol.Optional(CONF_LAST_RESET_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_LAST_RESET_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + } + ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema), +) async def async_setup_platform( diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 15ca9870077..724dec1c93f 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -306,6 +306,28 @@ async def test_setting_sensor_last_reset_via_mqtt_json_message(hass, mqtt_mock): assert state.attributes.get("last_reset") == "2020-01-02T08:11:00" +async def test_last_reset_deprecated(hass, mqtt_mock, caplog): + """Test the setting of the last_reset property via MQTT.""" + assert await async_setup_component( + hass, + sensor.DOMAIN, + { + sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "last_reset_topic": "last-reset-topic", + "last_reset_value_template": "{{ value_json.last_reset }}", + } + }, + ) + await hass.async_block_till_done() + + assert "The 'last_reset_topic' option is deprecated" in caplog.text + assert "The 'last_reset_value_template' option is deprecated" in caplog.text + + async def test_force_update_disabled(hass, mqtt_mock): """Test force update option.""" assert await async_setup_component( From 98e8e893649b020748fbdb97d1fad57205e1e1c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 18 Aug 2021 21:30:37 +0200 Subject: [PATCH 343/355] Mill data coordinator (#53603) Co-authored-by: Paulus Schoutsen --- homeassistant/components/mill/__init__.py | 35 ++++- homeassistant/components/mill/climate.py | 163 +++++++++----------- homeassistant/components/mill/manifest.json | 2 +- homeassistant/components/mill/sensor.py | 38 ++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 126 insertions(+), 116 deletions(-) diff --git a/homeassistant/components/mill/__init__.py b/homeassistant/components/mill/__init__.py index 75422cd26e1..73cb65daf05 100644 --- a/homeassistant/components/mill/__init__.py +++ b/homeassistant/components/mill/__init__.py @@ -1,15 +1,43 @@ """The mill component.""" +from datetime import timedelta +import logging + from mill import Mill from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +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 = ["climate", "sensor"] +class MillDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Mill data.""" + + def __init__( + self, + hass: HomeAssistant, + *, + mill_data_connection: Mill, + ) -> None: + """Initialize global Mill data updater.""" + self.mill_data_connection = mill_data_connection + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_method=mill_data_connection.fetch_heater_data, + update_interval=timedelta(seconds=30), + ) + + async def async_setup_entry(hass, entry): """Set up the Mill heater.""" mill_data_connection = Mill( @@ -20,9 +48,12 @@ async def async_setup_entry(hass, entry): if not await mill_data_connection.connect(): raise ConfigEntryNotReady - await mill_data_connection.find_all_heaters() + hass.data[DOMAIN] = MillDataUpdateCoordinator( + hass, + mill_data_connection=mill_data_connection, + ) - hass.data[DOMAIN] = mill_data_connection + await hass.data[DOMAIN].async_config_entry_first_refresh() hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 16c78329b0b..199bdf393a1 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -11,8 +11,10 @@ from homeassistant.components.climate.const import ( SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_AWAY_TEMP, @@ -41,11 +43,11 @@ SET_ROOM_TEMP_SCHEMA = vol.Schema( async def async_setup_entry(hass, entry, async_add_entities): """Set up the Mill climate.""" - mill_data_connection = hass.data[DOMAIN] + mill_data_coordinator = hass.data[DOMAIN] dev = [] - for heater in mill_data_connection.heaters.values(): - dev.append(MillHeater(heater, mill_data_connection)) + for heater in mill_data_coordinator.data.values(): + dev.append(MillHeater(mill_data_coordinator, heater)) async_add_entities(dev) async def set_room_temp(service): @@ -54,7 +56,7 @@ async def async_setup_entry(hass, entry, async_add_entities): sleep_temp = service.data.get(ATTR_SLEEP_TEMP) comfort_temp = service.data.get(ATTR_COMFORT_TEMP) away_temp = service.data.get(ATTR_AWAY_TEMP) - await mill_data_connection.set_room_temperatures_by_name( + await mill_data_coordinator.mill_data_connection.set_room_temperatures_by_name( room_name, sleep_temp, comfort_temp, away_temp ) @@ -63,122 +65,97 @@ async def async_setup_entry(hass, entry, async_add_entities): ) -class MillHeater(ClimateEntity): +class MillHeater(CoordinatorEntity, ClimateEntity): """Representation of a Mill Thermostat device.""" _attr_fan_modes = [FAN_ON, HVAC_MODE_OFF] _attr_max_temp = MAX_TEMP _attr_min_temp = MIN_TEMP _attr_supported_features = SUPPORT_FLAGS - _attr_target_temperature_step = 1 + _attr_target_temperature_step = PRECISION_WHOLE _attr_temperature_unit = TEMP_CELSIUS - def __init__(self, heater, mill_data_connection): + def __init__(self, coordinator, heater): """Initialize the thermostat.""" - self._heater = heater - self._conn = mill_data_connection + super().__init__(coordinator) + + self._id = heater.device_id self._attr_unique_id = heater.device_id self._attr_name = heater.name - - @property - def available(self): - """Return True if entity is available.""" - return self._heater.available - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - res = { - "open_window": self._heater.open_window, - "heating": self._heater.is_heating, - "controlled_by_tibber": self._heater.tibber_control, - "heater_generation": 1 if self._heater.is_gen1 else 2, + self._attr_device_info = { + "identifiers": {(DOMAIN, heater.device_id)}, + "name": self.name, + "manufacturer": MANUFACTURER, + "model": f"generation {1 if heater.is_gen1 else 2}", } - if self._heater.room: - res["room"] = self._heater.room.name - res["avg_room_temp"] = self._heater.room.avg_temp + if heater.is_gen1: + self._attr_hvac_modes = [HVAC_MODE_HEAT] else: - res["room"] = "Independent device" - return res - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._heater.set_temp - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._heater.current_temp - - @property - def fan_mode(self): - """Return the fan setting.""" - return FAN_ON if self._heater.fan_status == 1 else HVAC_MODE_OFF - - @property - def hvac_action(self): - """Return current hvac i.e. heat, cool, idle.""" - if self._heater.is_gen1 or self._heater.is_heating == 1: - return CURRENT_HVAC_HEAT - return CURRENT_HVAC_IDLE - - @property - def hvac_mode(self) -> str: - """Return hvac operation ie. heat, cool mode. - - Need to be one of HVAC_MODE_*. - """ - if self._heater.is_gen1 or self._heater.power_status == 1: - return HVAC_MODE_HEAT - return HVAC_MODE_OFF - - @property - def hvac_modes(self): - """Return the list of available hvac operation modes. - - Need to be a subset of HVAC_MODES. - """ - if self._heater.is_gen1: - return [HVAC_MODE_HEAT] - return [HVAC_MODE_HEAT, HVAC_MODE_OFF] + self._attr_hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_OFF] + self._update_attr(heater) async def async_set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: return - await self._conn.set_heater_temp(self._heater.device_id, int(temperature)) + await self.coordinator.mill_data_connection.set_heater_temp( + self._id, int(temperature) + ) + await self.coordinator.async_request_refresh() async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" fan_status = 1 if fan_mode == FAN_ON else 0 - await self._conn.heater_control(self._heater.device_id, fan_status=fan_status) + await self.coordinator.mill_data_connection.heater_control( + self._id, fan_status=fan_status + ) + await self.coordinator.async_request_refresh() async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" + heater = self.coordinator.data[self._id] + if hvac_mode == HVAC_MODE_HEAT: - await self._conn.heater_control(self._heater.device_id, power_status=1) - elif hvac_mode == HVAC_MODE_OFF and not self._heater.is_gen1: - await self._conn.heater_control(self._heater.device_id, power_status=0) + await self.coordinator.mill_data_connection.heater_control( + self._id, power_status=1 + ) + await self.coordinator.async_request_refresh() + elif hvac_mode == HVAC_MODE_OFF and not heater.is_gen1: + await self.coordinator.mill_data_connection.heater_control( + self._id, power_status=0 + ) + await self.coordinator.async_request_refresh() - async def async_update(self): - """Retrieve latest state.""" - self._heater = await self._conn.update_device(self._heater.device_id) + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_attr(self.coordinator.data[self._id]) + self.async_write_ha_state() - @property - def device_id(self): - """Return the ID of the physical device this sensor is part of.""" - return self._heater.device_id - - @property - def device_info(self): - """Return the device_info of the device.""" - device_info = { - "identifiers": {(DOMAIN, self.device_id)}, - "name": self.name, - "manufacturer": MANUFACTURER, - "model": f"generation {1 if self._heater.is_gen1 else 2}", + @callback + def _update_attr(self, heater): + self._attr_available = heater.available + self._attr_extra_state_attributes = { + "open_window": heater.open_window, + "heating": heater.is_heating, + "controlled_by_tibber": heater.tibber_control, + "heater_generation": 1 if heater.is_gen1 else 2, } - return device_info + if heater.room: + self._attr_extra_state_attributes["room"] = heater.room.name + self._attr_extra_state_attributes["avg_room_temp"] = heater.room.avg_temp + else: + self._attr_extra_state_attributes["room"] = "Independent device" + self._attr_target_temperature = heater.set_temp + self._attr_current_temperature = heater.current_temp + self._attr_fan_mode = FAN_ON if heater.fan_status == 1 else HVAC_MODE_OFF + if heater.is_gen1 or heater.is_heating == 1: + self._attr_hvac_action = CURRENT_HVAC_HEAT + else: + self._attr_hvac_action = CURRENT_HVAC_IDLE + if heater.is_gen1 or heater.power_status == 1: + self._attr_hvac_mode = HVAC_MODE_HEAT + else: + self._attr_hvac_mode = HVAC_MODE_OFF diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index 161bbe274ef..33a7c35c169 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -2,7 +2,7 @@ "domain": "mill", "name": "Mill", "documentation": "https://www.home-assistant.io/integrations/mill", - "requirements": ["millheater==0.5.0"], + "requirements": ["millheater==0.5.2"], "codeowners": ["@danielhiversen"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index ce7704ad1be..11b006e4b6e 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -6,6 +6,8 @@ from homeassistant.components.sensor import ( SensorEntity, ) from homeassistant.const import ENERGY_KILO_WATT_HOUR +from homeassistant.core import callback +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONSUMPTION_TODAY, CONSUMPTION_YEAR, DOMAIN, MANUFACTURER @@ -13,27 +15,28 @@ from .const import CONSUMPTION_TODAY, CONSUMPTION_YEAR, DOMAIN, MANUFACTURER async def async_setup_entry(hass, entry, async_add_entities): """Set up the Mill sensor.""" - mill_data_connection = hass.data[DOMAIN] + mill_data_coordinator = hass.data[DOMAIN] entities = [ - MillHeaterEnergySensor(heater, mill_data_connection, sensor_type) + MillHeaterEnergySensor(mill_data_coordinator, sensor_type, heater) for sensor_type in (CONSUMPTION_TODAY, CONSUMPTION_YEAR) - for heater in mill_data_connection.heaters.values() + for heater in mill_data_coordinator.data.values() ] async_add_entities(entities) -class MillHeaterEnergySensor(SensorEntity): +class MillHeaterEnergySensor(CoordinatorEntity, SensorEntity): """Representation of a Mill Sensor device.""" _attr_device_class = DEVICE_CLASS_ENERGY _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR _attr_state_class = STATE_CLASS_TOTAL_INCREASING - def __init__(self, heater, mill_data_connection, sensor_type): + def __init__(self, coordinator, sensor_type, heater): """Initialize the sensor.""" + super().__init__(coordinator) + self._id = heater.device_id - self._conn = mill_data_connection self._sensor_type = sensor_type self._attr_name = f"{heater.name} {sensor_type.replace('_', ' ')}" @@ -44,20 +47,19 @@ class MillHeaterEnergySensor(SensorEntity): "manufacturer": MANUFACTURER, "model": f"generation {1 if heater.is_gen1 else 2}", } + self._update_attr(heater) - async def async_update(self): - """Retrieve latest state.""" - heater = await self._conn.update_device(self._id) + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_attr(self.coordinator.data[self._id]) + self.async_write_ha_state() + + @callback + def _update_attr(self, heater): self._attr_available = heater.available if self._sensor_type == CONSUMPTION_TODAY: - _state = heater.day_consumption + self._attr_native_value = heater.day_consumption elif self._sensor_type == CONSUMPTION_YEAR: - _state = heater.year_consumption - else: - _state = None - if _state is None: - self._attr_native_value = _state - return - - self._attr_native_value = _state + self._attr_native_value = heater.year_consumption diff --git a/requirements_all.txt b/requirements_all.txt index 69da5abc72d..9ec1ccce7c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -980,7 +980,7 @@ micloud==0.3 miflora==0.7.0 # homeassistant.components.mill -millheater==0.5.0 +millheater==0.5.2 # homeassistant.components.minio minio==4.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e476881426c..710f4e9ae2f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -551,7 +551,7 @@ mficlient==0.3.0 micloud==0.3 # homeassistant.components.mill -millheater==0.5.0 +millheater==0.5.2 # homeassistant.components.minio minio==4.0.9 From c74f9a8313c621c9d8c777b07216c286d55d6118 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Aug 2021 21:47:57 +0200 Subject: [PATCH 344/355] Remove stale references to last_reset (#54838) * Remove stale references to last_reset * Update tests --- homeassistant/components/kostal_plenticore/const.py | 3 --- tests/components/dsmr/test_sensor.py | 13 ------------- tests/components/mysensors/test_sensor.py | 2 -- 3 files changed, 18 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/const.py b/homeassistant/components/kostal_plenticore/const.py index 5cbc1a2af79..9f902da7d2f 100644 --- a/homeassistant/components/kostal_plenticore/const.py +++ b/homeassistant/components/kostal_plenticore/const.py @@ -16,14 +16,11 @@ from homeassistant.const import ( PERCENTAGE, POWER_WATT, ) -from homeassistant.util.dt import utc_from_timestamp DOMAIN = "kostal_plenticore" ATTR_ENABLED_DEFAULT = "entity_registry_enabled_default" -LAST_RESET_NEVER = utc_from_timestamp(0) - # Defines all entities for process data. # # Each entry is defined with a tuple of these values: diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 88e984cea1b..6d40437d87a 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -14,7 +14,6 @@ from unittest.mock import DEFAULT, MagicMock from homeassistant import config_entries from homeassistant.components.dsmr.const import DOMAIN from homeassistant.components.sensor import ( - ATTR_LAST_RESET, ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, STATE_CLASS_MEASUREMENT, @@ -137,7 +136,6 @@ async def test_default_setup(hass, dsmr_connection_fixture): assert power_consumption.state == STATE_UNKNOWN assert power_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER assert power_consumption.attributes.get(ATTR_ICON) is None - assert power_consumption.attributes.get(ATTR_LAST_RESET) is None assert power_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert power_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None @@ -159,7 +157,6 @@ async def test_default_setup(hass, dsmr_connection_fixture): assert power_tariff.state == "low" assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_LAST_RESET) is None assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" @@ -167,7 +164,6 @@ async def test_default_setup(hass, dsmr_connection_fixture): gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS - assert gas_consumption.attributes.get(ATTR_LAST_RESET) is None assert ( gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING ) @@ -259,7 +255,6 @@ async def test_v4_meter(hass, dsmr_connection_fixture): assert power_tariff.state == "low" assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_LAST_RESET) is None assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" @@ -268,7 +263,6 @@ async def test_v4_meter(hass, dsmr_connection_fixture): assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS - assert gas_consumption.attributes.get(ATTR_LAST_RESET) is None assert ( gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING ) @@ -331,7 +325,6 @@ async def test_v5_meter(hass, dsmr_connection_fixture): assert power_tariff.state == "low" assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_LAST_RESET) is None assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" @@ -339,7 +332,6 @@ async def test_v5_meter(hass, dsmr_connection_fixture): gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS - assert gas_consumption.attributes.get(ATTR_LAST_RESET) is None assert ( gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING ) @@ -407,7 +399,6 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture): assert power_tariff.state == "123.456" assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY assert power_tariff.attributes.get(ATTR_ICON) is None - assert power_tariff.attributes.get(ATTR_LAST_RESET) is None assert power_tariff.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING assert ( power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -421,7 +412,6 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture): gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS - assert gas_consumption.attributes.get(ATTR_LAST_RESET) is None assert ( gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING ) @@ -484,7 +474,6 @@ async def test_belgian_meter(hass, dsmr_connection_fixture): assert power_tariff.state == "normal" assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_LAST_RESET) is None assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" @@ -492,7 +481,6 @@ async def test_belgian_meter(hass, dsmr_connection_fixture): gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is DEVICE_CLASS_GAS - assert gas_consumption.attributes.get(ATTR_LAST_RESET) is None assert ( gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING ) @@ -544,7 +532,6 @@ async def test_belgian_meter_low(hass, dsmr_connection_fixture): assert power_tariff.state == "low" assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_LAST_RESET) is None assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" diff --git a/tests/components/mysensors/test_sensor.py b/tests/components/mysensors/test_sensor.py index 18d88a24206..d648aebdefd 100644 --- a/tests/components/mysensors/test_sensor.py +++ b/tests/components/mysensors/test_sensor.py @@ -7,7 +7,6 @@ from mysensors.sensor import Sensor import pytest from homeassistant.components.sensor import ( - ATTR_LAST_RESET, ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, @@ -75,7 +74,6 @@ async def test_power_sensor( assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_POWER assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT - assert ATTR_LAST_RESET not in state.attributes async def test_energy_sensor( From c75c4aeea54f451b07a9428d12da61af606ebcf4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 Aug 2021 13:56:43 -0700 Subject: [PATCH 345/355] Bump frontend to 20210818.0 (#54851) --- 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 a4a97914622..a83e3572828 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==20210813.0" + "home-assistant-frontend==20210818.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 213503a92c5..7e4e57f608f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ cryptography==3.3.2 defusedxml==0.7.1 emoji==1.2.0 hass-nabucasa==0.46.0 -home-assistant-frontend==20210813.0 +home-assistant-frontend==20210818.0 httpx==0.18.2 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 9ec1ccce7c3..37377856e5f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -787,7 +787,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210813.0 +home-assistant-frontend==20210818.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 710f4e9ae2f..48151c9392f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -453,7 +453,7 @@ hole==0.5.1 holidays==0.11.2 # homeassistant.components.frontend -home-assistant-frontend==20210813.0 +home-assistant-frontend==20210818.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 799a97789d4390b1feeaf678942248da12b579e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Aug 2021 16:40:45 -0500 Subject: [PATCH 346/355] Drop tado codeowner (#54849) - I no longer have any tado devices --- CODEOWNERS | 2 +- homeassistant/components/tado/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 85b89649a99..3606fade468 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -506,7 +506,7 @@ homeassistant/components/synology_dsm/* @hacf-fr @Quentame @mib1185 homeassistant/components/synology_srm/* @aerialls homeassistant/components/syslog/* @fabaff homeassistant/components/system_bridge/* @timmo001 -homeassistant/components/tado/* @michaelarnauts @bdraco @noltari +homeassistant/components/tado/* @michaelarnauts @noltari homeassistant/components/tag/* @balloob @dmulcahey homeassistant/components/tahoma/* @philklei homeassistant/components/tankerkoenig/* @guillempages diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 758756e8127..8cf0ed260e8 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -3,7 +3,7 @@ "name": "Tado", "documentation": "https://www.home-assistant.io/integrations/tado", "requirements": ["python-tado==0.10.0"], - "codeowners": ["@michaelarnauts", "@bdraco", "@noltari"], + "codeowners": ["@michaelarnauts", "@noltari"], "config_flow": true, "homekit": { "models": ["tado", "AC02"] From 4a9a6cd5389a8578efdc78d580d1dfbf5a18ded0 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 19 Aug 2021 00:13:56 +0000 Subject: [PATCH 347/355] [ci skip] Translation update --- .../components/airtouch4/translations/cs.json | 17 +++++++++++++++++ .../components/airtouch4/translations/de.json | 19 +++++++++++++++++++ .../components/airtouch4/translations/it.json | 19 +++++++++++++++++++ .../components/airtouch4/translations/nl.json | 17 +++++++++++++++++ .../airtouch4/translations/zh-Hant.json | 19 +++++++++++++++++++ .../binary_sensor/translations/it.json | 8 ++++++++ .../binary_sensor/translations/nl.json | 5 +++++ .../components/sensor/translations/it.json | 18 ++++++++++++++++++ .../components/sensor/translations/nl.json | 16 ++++++++++++++++ .../components/tractive/translations/it.json | 4 +++- .../components/tractive/translations/nl.json | 4 +++- .../uptimerobot/translations/it.json | 17 +++++++++++++++-- 12 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/airtouch4/translations/cs.json create mode 100644 homeassistant/components/airtouch4/translations/de.json create mode 100644 homeassistant/components/airtouch4/translations/it.json create mode 100644 homeassistant/components/airtouch4/translations/nl.json create mode 100644 homeassistant/components/airtouch4/translations/zh-Hant.json diff --git a/homeassistant/components/airtouch4/translations/cs.json b/homeassistant/components/airtouch4/translations/cs.json new file mode 100644 index 00000000000..6fabc170b6e --- /dev/null +++ b/homeassistant/components/airtouch4/translations/cs.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/de.json b/homeassistant/components/airtouch4/translations/de.json new file mode 100644 index 00000000000..84f93d09962 --- /dev/null +++ b/homeassistant/components/airtouch4/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "no_units": "Es konnten keine AirTouch 4-Gruppen gefunden werden." + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "title": "Richte deine AirTouch 4-Verbindungsdetails ein." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/it.json b/homeassistant/components/airtouch4/translations/it.json new file mode 100644 index 00000000000..f9a72a50e33 --- /dev/null +++ b/homeassistant/components/airtouch4/translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "no_units": "Impossibile trovare alcun gruppo AirTouch 4." + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "title": "Imposta i dettagli della connessione AirTouch 4." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/nl.json b/homeassistant/components/airtouch4/translations/nl.json new file mode 100644 index 00000000000..d6137499b3e --- /dev/null +++ b/homeassistant/components/airtouch4/translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "no_units": "Kan geen AirTouch 4-groepen vinden." + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/zh-Hant.json b/homeassistant/components/airtouch4/translations/zh-Hant.json new file mode 100644 index 00000000000..9ac310b531b --- /dev/null +++ b/homeassistant/components/airtouch4/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "no_units": "\u627e\u4e0d\u5230\u4efb\u4f55 AirTouch 4 \u7fa4\u7d44\u3002" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "title": "\u8a2d\u5b9a AirTouch 4 \u9023\u7dda\u8cc7\u8a0a\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/it.json b/homeassistant/components/binary_sensor/translations/it.json index 68c427cbc04..b6301ed8f62 100644 --- a/homeassistant/components/binary_sensor/translations/it.json +++ b/homeassistant/components/binary_sensor/translations/it.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} non sta rilevando un problema", "is_no_smoke": "{entity_name} non sta rilevando il fumo", "is_no_sound": "{entity_name} non sta rilevando il suono", + "is_no_update": "{entity_name} \u00e8 aggiornato", "is_no_vibration": "{entity_name} non sta rilevando la vibrazione", "is_not_bat_low": "{entity_name} la batteria \u00e8 normale", "is_not_cold": "{entity_name} non \u00e8 freddo", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name} sta rilevando il fumo", "is_sound": "{entity_name} sta rilevando il suono", "is_unsafe": "{entity_name} non \u00e8 sicuro", + "is_update": "{entity_name} ha un aggiornamento disponibile", "is_vibration": "{entity_name} sta rilevando la vibrazione" }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name} ha smesso di rilevare un problema", "no_smoke": "{entity_name} ha smesso la rilevazione di fumo", "no_sound": "{entity_name} ha smesso di rilevare il suono", + "no_update": "{entity_name} \u00e8 diventato aggiornato", "no_vibration": "{entity_name} ha smesso di rilevare le vibrazioni", "not_bat_low": "{entity_name} batteria normale", "not_cold": "{entity_name} non \u00e8 diventato freddo", @@ -86,6 +89,7 @@ "turned_off": "{entity_name} disattivato", "turned_on": "{entity_name} attivato", "unsafe": "{entity_name} diventato non sicuro", + "update": "{entity_name} ha ottenuto un aggiornamento disponibile", "vibration": "{entity_name} iniziato a rilevare le vibrazioni" } }, @@ -178,6 +182,10 @@ "off": "Assente", "on": "Rilevato" }, + "update": { + "off": "Aggiornato", + "on": "Aggiornamento disponibile" + }, "vibration": { "off": "Assente", "on": "Rilevata" diff --git a/homeassistant/components/binary_sensor/translations/nl.json b/homeassistant/components/binary_sensor/translations/nl.json index 9352bfa8d47..b44dd3449eb 100644 --- a/homeassistant/components/binary_sensor/translations/nl.json +++ b/homeassistant/components/binary_sensor/translations/nl.json @@ -42,6 +42,7 @@ "is_smoke": "{entity_name} detecteert rook", "is_sound": "{entity_name} detecteert geluid", "is_unsafe": "{entity_name} is onveilig", + "is_update": "{entity_name} heeft een update beschikbaar", "is_vibration": "{entity_name} detecteert trillingen" }, "trigger_type": { @@ -86,6 +87,7 @@ "turned_off": "{entity_name} uitgeschakeld", "turned_on": "{entity_name} ingeschakeld", "unsafe": "{entity_name} werd onveilig", + "update": "{entity_name} kreeg een update beschikbaar", "vibration": "{entity_name} begon trillingen te detecteren" } }, @@ -178,6 +180,9 @@ "off": "Niet gedetecteerd", "on": "Gedetecteerd" }, + "update": { + "on": "Update beschikbaar" + }, "vibration": { "off": "Niet gedetecteerd", "on": "Gedetecteerd" diff --git a/homeassistant/components/sensor/translations/it.json b/homeassistant/components/sensor/translations/it.json index 6ae19c201d7..7b9b483c024 100644 --- a/homeassistant/components/sensor/translations/it.json +++ b/homeassistant/components/sensor/translations/it.json @@ -6,12 +6,21 @@ "is_carbon_monoxide": "Livello attuale di concentrazione di monossido di carbonio in {entity_name}", "is_current": "Corrente attuale di {entity_name}", "is_energy": "Energia attuale di {entity_name}", + "is_gas": "Attuale gas di {entity_name}", "is_humidity": "Umidit\u00e0 attuale di {entity_name}", "is_illuminance": "Illuminazione attuale di {entity_name}", + "is_nitrogen_dioxide": "Attuale livello di concentrazione di biossido di azoto di {entity_name}", + "is_nitrogen_monoxide": "Attuale livello di concentrazione di monossido di azoto di {entity_name}", + "is_nitrous_oxide": "Attuale livello di concentrazione di ossidi di azoto di {entity_name}", + "is_ozone": "Attuale livello di concentrazione di ozono di {entity_name}", + "is_pm1": "Attuale livello di concentrazione di PM1 di {entity_name}", + "is_pm10": "Attuale livello di concentrazione di PM10 di {entity_name}", + "is_pm25": "Attuale livello di concentrazione di PM2.5 di {entity_name}", "is_power": "Alimentazione attuale di {entity_name}", "is_power_factor": "Fattore di potenza attuale di {entity_name}", "is_pressure": "Pressione attuale di {entity_name}", "is_signal_strength": "Potenza del segnale attuale di {entity_name}", + "is_sulphur_dioxide": "Attuale livello di concentrazione di anidride solforosa di {entity_name}", "is_temperature": "Temperatura attuale di {entity_name}", "is_value": "Valore attuale di {entity_name}", "is_voltage": "Tensione attuale di {entity_name}" @@ -22,12 +31,21 @@ "carbon_monoxide": "Variazioni nella concentrazione di monossido di carbonio di {entity_name}", "current": "variazioni di corrente di {entity_name}", "energy": "variazioni di energia di {entity_name}", + "gas": "Variazioni di gas di {entity_name}", "humidity": "variazioni di umidit\u00e0 di {entity_name} ", "illuminance": "variazioni dell'illuminazione di {entity_name}", + "nitrogen_dioxide": "Variazioni della concentrazione di biossido di azoto di {entity_name}", + "nitrogen_monoxide": "Variazioni della concentrazione di monossido di azoto di {entity_name}", + "nitrous_oxide": "Variazioni della concentrazione di ossidi di azoto di {entity_name}", + "ozone": "Variazioni della concentrazione di ozono di {entity_name}", + "pm1": "Variazioni della concentrazione di PM1 di {entity_name}", + "pm10": "Variazioni della concentrazione di PM10 di {entity_name}", + "pm25": "Variazioni della concentrazione di PM2.5 di {entity_name}", "power": "variazioni di alimentazione di {entity_name}", "power_factor": "variazioni del fattore di potenza di {entity_name}", "pressure": "variazioni della pressione di {entity_name}", "signal_strength": "variazioni della potenza del segnale di {entity_name}", + "sulphur_dioxide": "Variazioni della concentrazione di anidride solforosa di {entity_name}", "temperature": "variazioni di temperatura di {entity_name}", "value": "{entity_name} valori cambiati", "voltage": "variazioni di tensione di {entity_name}" diff --git a/homeassistant/components/sensor/translations/nl.json b/homeassistant/components/sensor/translations/nl.json index 933caf15de8..c3ab0bf5bfa 100644 --- a/homeassistant/components/sensor/translations/nl.json +++ b/homeassistant/components/sensor/translations/nl.json @@ -9,10 +9,18 @@ "is_gas": "Huidig {entity_name} gas", "is_humidity": "Huidige {entity_name} vochtigheidsgraad", "is_illuminance": "Huidige {entity_name} verlichtingssterkte", + "is_nitrogen_dioxide": "Huidige {entity_name} stikstofdioxideconcentratie", + "is_nitrogen_monoxide": "Huidige {entity_name} stikstofmonoxideconcentratie", + "is_nitrous_oxide": "Huidige {entity_name} distikstofmonoxideconcentratie", + "is_ozone": "Huidige {entity_name} ozonconcentratie", + "is_pm1": "Huidige {entity_name} PM1-concentratie", + "is_pm10": "Huidige {entity_name} PM10-concentratie", + "is_pm25": "Huidige {entity_name} PM2.5-concentratie", "is_power": "Huidige {entity_name}\nvermogen", "is_power_factor": "Huidige {entity_name} vermogensfactor", "is_pressure": "Huidige {entity_name} druk", "is_signal_strength": "Huidige {entity_name} signaalsterkte", + "is_sulphur_dioxide": "Huidige {entity_name} zwaveldioxideconcentratie", "is_temperature": "Huidige {entity_name} temperatuur", "is_value": "Huidige {entity_name} waarde", "is_voltage": "Huidige {entity_name} spanning" @@ -26,10 +34,18 @@ "gas": "{entity_name} gas verandert", "humidity": "{entity_name} vochtigheidsgraad gewijzigd", "illuminance": "{entity_name} verlichtingssterkte gewijzigd", + "nitrogen_dioxide": "{entity_name} stikstofdioxideconcentratieverandering", + "nitrogen_monoxide": "{entity_name} stikstofmonoxideconcentratieverandering", + "nitrous_oxide": "{entity_name} distikstofmonoxideconcentratieverandering", + "ozone": "{entity_name} ozonconcentratieveranderingen", + "pm1": "{entity_name} PM1-concentratieveranderingen", + "pm10": "{entity_name} PM10-concentratieveranderingen", + "pm25": "{entity_name} PM2.5-concentratieveranderingen", "power": "{entity_name} vermogen gewijzigd", "power_factor": "{entity_name} power factor verandert", "pressure": "{entity_name} druk gewijzigd", "signal_strength": "{entity_name} signaalsterkte gewijzigd", + "sulphur_dioxide": "{entity_name} zwaveldioxideconcentratieveranderingen", "temperature": "{entity_name} temperatuur gewijzigd", "value": "{entity_name} waarde gewijzigd", "voltage": "{entity_name} voltage verandert" diff --git a/homeassistant/components/tractive/translations/it.json b/homeassistant/components/tractive/translations/it.json index 484d1e229e2..44cdc2df3d7 100644 --- a/homeassistant/components/tractive/translations/it.json +++ b/homeassistant/components/tractive/translations/it.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_failed_existing": "Impossibile aggiornare la voce di configurazione, rimuovere l'integrazione e configurarla di nuovo.", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "invalid_auth": "Autenticazione non valida", diff --git a/homeassistant/components/tractive/translations/nl.json b/homeassistant/components/tractive/translations/nl.json index 2ae14092cde..b0e1f17cdc3 100644 --- a/homeassistant/components/tractive/translations/nl.json +++ b/homeassistant/components/tractive/translations/nl.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "reauth_failed_existing": "Kon het configuratie-item niet bijwerken, verwijder de integratie en stel deze opnieuw in.", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "invalid_auth": "Ongeldige authenticatie", diff --git a/homeassistant/components/uptimerobot/translations/it.json b/homeassistant/components/uptimerobot/translations/it.json index 6b151199afe..517bbf6463f 100644 --- a/homeassistant/components/uptimerobot/translations/it.json +++ b/homeassistant/components/uptimerobot/translations/it.json @@ -1,17 +1,30 @@ { "config": { "abort": { - "already_configured": "L'account \u00e8 gi\u00e0 configurato" + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_failed_existing": "Impossibile aggiornare la voce di configurazione, rimuovere l'integrazione e configurarla di nuovo.", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "unknown": "Errore imprevisto" }, "error": { "cannot_connect": "Impossibile connettersi", + "invalid_api_key": "Chiave API non valida", + "reauth_failed_matching_account": "La chiave API che hai fornito non corrisponde all'ID account per la configurazione esistente.", "unknown": "Errore imprevisto" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Chiave API" + }, + "description": "Devi fornire una nuova chiave API di sola lettura da Uptime Robot", + "title": "Autenticare nuovamente l'integrazione" + }, "user": { "data": { "api_key": "Chiave API" - } + }, + "description": "Devi fornire una chiave API di sola lettura da Uptime Robot" } } } From 4f9c7882166985534475230b6b823cf751eab05f Mon Sep 17 00:00:00 2001 From: Robin Wohlers-Reichel Date: Thu, 19 Aug 2021 11:50:28 +1000 Subject: [PATCH 348/355] Update PULL_REQUEST_TEMPLATE.md (#54762) * Update PULL_REQUEST_TEMPLATE.md * Update PULL_REQUEST_TEMPLATE.md Address review comments by moving changes into 'Checklist' section * Update PULL_REQUEST_TEMPLATE.md * Update .github/PULL_REQUEST_TEMPLATE.md Co-authored-by: Franck Nijhof * Update .github/PULL_REQUEST_TEMPLATE.md Co-authored-by: Martin Hjelmare Co-authored-by: Franck Nijhof Co-authored-by: Martin Hjelmare --- .github/PULL_REQUEST_TEMPLATE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7c169580cb2..974022834fb 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -71,6 +71,7 @@ If the code communicates with devices, web services, or third-party tools: Updated and included derived files by running: `python3 -m script.hassfest`. - [ ] New or updated dependencies have been added to `requirements_all.txt`. Updated by running `python3 -m script.gen_requirements_all`. +- [ ] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description. - [ ] Untested files have been added to `.coveragerc`. The integration reached or maintains the following [Integration Quality Scale][quality-scale]: From d3f7312834dc7dea4426cc3ed572384a37945e79 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Wed, 18 Aug 2021 19:50:46 -0600 Subject: [PATCH 349/355] Improve MyQ code quality through creation of MyQ entity (#54728) --- homeassistant/components/myq/__init__.py | 56 ++++++++++++++++++- homeassistant/components/myq/binary_sensor.py | 44 +-------------- homeassistant/components/myq/cover.py | 52 +++-------------- homeassistant/components/myq/light.py | 49 +--------------- 4 files changed, 67 insertions(+), 134 deletions(-) diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index 063f044117e..253c10544c9 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -3,6 +3,13 @@ from datetime import timedelta import logging import pymyq +from pymyq.const import ( + DEVICE_STATE as MYQ_DEVICE_STATE, + DEVICE_STATE_ONLINE as MYQ_DEVICE_STATE_ONLINE, + KNOWN_MODELS, + MANUFACTURER, +) +from pymyq.device import MyQDevice from pymyq.errors import InvalidCredentialsError, MyQError from homeassistant.config_entries import ConfigEntry @@ -10,7 +17,11 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME 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.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, PLATFORMS, UPDATE_INTERVAL @@ -63,3 +74,46 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +class MyQEntity(CoordinatorEntity): + """Base class for MyQ Entities.""" + + def __init__(self, coordinator: DataUpdateCoordinator, device: MyQDevice) -> None: + """Initialize class.""" + super().__init__(coordinator) + self._device = device + self._attr_unique_id = device.device_id + + @property + def name(self): + """Return the name if any, name can change if user changes it within MyQ.""" + return self._device.name + + @property + def device_info(self): + """Return the device_info of the device.""" + device_info = { + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self._device.name, + "manufacturer": MANUFACTURER, + "sw_version": self._device.firmware_version, + } + model = ( + KNOWN_MODELS.get(self._device.device_id[2:4]) + if self._device.device_id is not None + else None + ) + if model: + device_info["model"] = model + if self._device.parent_device_id: + device_info["via_device"] = (DOMAIN, self._device.parent_device_id) + return device_info + + @property + def available(self): + """Return if the device is online.""" + # Not all devices report online so assume True if its missing + return super().available and self._device.device_json[MYQ_DEVICE_STATE].get( + MYQ_DEVICE_STATE_ONLINE, True + ) diff --git a/homeassistant/components/myq/binary_sensor.py b/homeassistant/components/myq/binary_sensor.py index 96ab589253b..9f2d766fcc4 100644 --- a/homeassistant/components/myq/binary_sensor.py +++ b/homeassistant/components/myq/binary_sensor.py @@ -1,17 +1,10 @@ """Support for MyQ gateways.""" -from pymyq.const import ( - DEVICE_STATE as MYQ_DEVICE_STATE, - DEVICE_STATE_ONLINE as MYQ_DEVICE_STATE_ONLINE, - KNOWN_MODELS, - MANUFACTURER, -) - from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, BinarySensorEntity, ) -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import MyQEntity from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY @@ -29,16 +22,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) -class MyQBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): +class MyQBinarySensorEntity(MyQEntity, BinarySensorEntity): """Representation of a MyQ gateway.""" _attr_device_class = DEVICE_CLASS_CONNECTIVITY - def __init__(self, coordinator, device): - """Initialize with API object, device id.""" - super().__init__(coordinator) - self._device = device - @property def name(self): """Return the name of the garage door if any.""" @@ -47,35 +35,9 @@ class MyQBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): @property def is_on(self): """Return if the device is online.""" - if not self.coordinator.last_update_success: - return False - - # Not all devices report online so assume True if its missing - return self._device.device_json[MYQ_DEVICE_STATE].get( - MYQ_DEVICE_STATE_ONLINE, True - ) + return super().available @property def available(self) -> bool: """Entity is always available.""" return True - - @property - def unique_id(self): - """Return a unique, Home Assistant friendly identifier for this entity.""" - return self._device.device_id - - @property - def device_info(self): - """Return the device_info of the device.""" - device_info = { - "identifiers": {(DOMAIN, self._device.device_id)}, - "name": self.name, - "manufacturer": MANUFACTURER, - "sw_version": self._device.firmware_version, - } - model = KNOWN_MODELS.get(self._device.device_id[2:4]) - if model: - device_info["model"] = model - - return device_info diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index 87b8223c477..e8e06dc3b22 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -1,13 +1,7 @@ """Support for MyQ-Enabled Garage Doors.""" import logging -from pymyq.const import ( - DEVICE_STATE as MYQ_DEVICE_STATE, - DEVICE_STATE_ONLINE as MYQ_DEVICE_STATE_ONLINE, - DEVICE_TYPE_GATE as MYQ_DEVICE_TYPE_GATE, - KNOWN_MODELS, - MANUFACTURER, -) +from pymyq.const import DEVICE_TYPE_GATE as MYQ_DEVICE_TYPE_GATE from pymyq.errors import MyQError from homeassistant.components.cover import ( @@ -19,8 +13,8 @@ from homeassistant.components.cover import ( ) from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import MyQEntity from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, MYQ_TO_HASS _LOGGER = logging.getLogger(__name__) @@ -33,16 +27,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): coordinator = data[MYQ_COORDINATOR] async_add_entities( - [MyQDevice(coordinator, device) for device in myq.covers.values()] + [MyQCover(coordinator, device) for device in myq.covers.values()] ) -class MyQDevice(CoordinatorEntity, CoverEntity): +class MyQCover(MyQEntity, CoverEntity): """Representation of a MyQ cover.""" + _attr_supported_features = SUPPORT_OPEN | SUPPORT_CLOSE + def __init__(self, coordinator, device): """Initialize with API object, device id.""" - super().__init__(coordinator) + super().__init__(coordinator, device) self._device = device if device.device_type == MYQ_DEVICE_TYPE_GATE: self._attr_device_class = DEVICE_CLASS_GATE @@ -50,19 +46,6 @@ class MyQDevice(CoordinatorEntity, CoverEntity): self._attr_device_class = DEVICE_CLASS_GARAGE self._attr_unique_id = device.device_id - @property - def name(self): - """Return the name of the garage door if any.""" - return self._device.name - - @property - def available(self): - """Return if the device is online.""" - # Not all devices report online so assume True if its missing - return super().available and self._device.device_json[MYQ_DEVICE_STATE].get( - MYQ_DEVICE_STATE_ONLINE, True - ) - @property def is_closed(self): """Return true if cover is closed, else False.""" @@ -83,11 +66,6 @@ class MyQDevice(CoordinatorEntity, CoverEntity): """Return if the cover is opening or not.""" return MYQ_TO_HASS.get(self._device.state) == STATE_OPENING - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_OPEN | SUPPORT_CLOSE - async def async_close_cover(self, **kwargs): """Issue close command to cover.""" if self.is_closing or self.is_closed: @@ -133,19 +111,3 @@ class MyQDevice(CoordinatorEntity, CoverEntity): if not result: raise HomeAssistantError(f"Opening of cover {self._device.name} failed") - - @property - def device_info(self): - """Return the device_info of the device.""" - device_info = { - "identifiers": {(DOMAIN, self._device.device_id)}, - "name": self._device.name, - "manufacturer": MANUFACTURER, - "sw_version": self._device.firmware_version, - } - model = KNOWN_MODELS.get(self._device.device_id[2:4]) - if model: - device_info["model"] = model - if self._device.parent_device_id: - device_info["via_device"] = (DOMAIN, self._device.parent_device_id) - return device_info diff --git a/homeassistant/components/myq/light.py b/homeassistant/components/myq/light.py index 98119c2157a..d8154d7c427 100644 --- a/homeassistant/components/myq/light.py +++ b/homeassistant/components/myq/light.py @@ -1,19 +1,13 @@ """Support for MyQ-Enabled lights.""" import logging -from pymyq.const import ( - DEVICE_STATE as MYQ_DEVICE_STATE, - DEVICE_STATE_ONLINE as MYQ_DEVICE_STATE_ONLINE, - KNOWN_MODELS, - MANUFACTURER, -) from pymyq.errors import MyQError from homeassistant.components.light import LightEntity from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import MyQEntity from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, MYQ_TO_HASS _LOGGER = logging.getLogger(__name__) @@ -30,29 +24,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class MyQLight(CoordinatorEntity, LightEntity): +class MyQLight(MyQEntity, LightEntity): """Representation of a MyQ light.""" _attr_supported_features = 0 - def __init__(self, coordinator, device): - """Initialize with API object, device id.""" - super().__init__(coordinator) - self._device = device - self._attr_unique_id = device.device_id - self._attr_name = device.name - - @property - def available(self): - """Return if the device is online.""" - if not super().available: - return False - - # Not all devices report online so assume True if its missing - return self._device.device_json[MYQ_DEVICE_STATE].get( - MYQ_DEVICE_STATE_ONLINE, True - ) - @property def is_on(self): """Return true if the light is on, else False.""" @@ -92,24 +68,3 @@ class MyQLight(CoordinatorEntity, LightEntity): # Write new state to HASS self.async_write_ha_state() - - @property - def device_info(self): - """Return the device_info of the device.""" - device_info = { - "identifiers": {(DOMAIN, self._device.device_id)}, - "name": self._device.name, - "manufacturer": MANUFACTURER, - "sw_version": self._device.firmware_version, - } - if model := KNOWN_MODELS.get(self._device.device_id[2:4]): - device_info["model"] = model - if self._device.parent_device_id: - device_info["via_device"] = (DOMAIN, self._device.parent_device_id) - return device_info - - async def async_added_to_hass(self): - """Subscribe to updates.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) From e11ffbcdafca67b2d0f4d3059e41851dd9ea6c00 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 18 Aug 2021 22:24:44 -0400 Subject: [PATCH 350/355] Rework goalzero for EntityDescription (#54786) * Rework goalzero for EntityDescription * changes * fix * lint --- homeassistant/components/goalzero/__init__.py | 21 +-- .../components/goalzero/binary_sensor.py | 78 +++++--- .../components/goalzero/config_flow.py | 15 +- homeassistant/components/goalzero/const.py | 141 --------------- homeassistant/components/goalzero/sensor.py | 166 +++++++++++++++--- homeassistant/components/goalzero/switch.py | 63 ++++--- 6 files changed, 252 insertions(+), 232 deletions(-) diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index 308934819cd..379a56512c6 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -43,7 +43,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [DOMAIN_BINARY_SENSOR, DOMAIN_SENSOR, DOMAIN_SWITCH] -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Goal Zero Yeti from a config entry.""" name = entry.data[CONF_NAME] host = entry.data[CONF_HOST] @@ -81,7 +81,7 @@ async def async_setup_entry(hass, entry): return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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) if unload_ok: @@ -94,7 +94,13 @@ class YetiEntity(CoordinatorEntity): _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} - def __init__(self, api, coordinator, name, server_unique_id): + def __init__( + self, + api: Yeti, + coordinator: DataUpdateCoordinator, + name: str, + server_unique_id: str, + ) -> None: """Initialize a Goal Zero Yeti entity.""" super().__init__(coordinator) self.api = api @@ -104,15 +110,10 @@ class YetiEntity(CoordinatorEntity): @property def device_info(self) -> DeviceInfo: """Return the device information of the entity.""" - model = sw_version = None - if self.api.sysdata: - model = self.api.sysdata[ATTR_MODEL] - if self.api.data: - sw_version = self.api.data["firmwareVersion"] return { ATTR_IDENTIFIERS: {(DOMAIN, self._server_unique_id)}, ATTR_MANUFACTURER: "Goal Zero", ATTR_NAME: self._name, - ATTR_MODEL: str(model), - ATTR_SW_VERSION: str(sw_version), + ATTR_MODEL: self.api.sysdata.get(ATTR_MODEL), + ATTR_SW_VERSION: self.api.data.get("firmwareVersion"), } diff --git a/homeassistant/components/goalzero/binary_sensor.py b/homeassistant/components/goalzero/binary_sensor.py index f9a110eff55..21eecc678ad 100644 --- a/homeassistant/components/goalzero/binary_sensor.py +++ b/homeassistant/components/goalzero/binary_sensor.py @@ -1,14 +1,51 @@ """Support for Goal Zero Yeti Sensors.""" -from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_NAME, CONF_NAME +from __future__ import annotations -from . import YetiEntity -from .const import BINARY_SENSOR_DICT, DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY_CHARGING, + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_POWER, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from . import Yeti, YetiEntity +from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN PARALLEL_UPDATES = 0 +BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription( + key="backlight", + name="Backlight", + icon="mdi:clock-digital", + ), + BinarySensorEntityDescription( + key="app_online", + name="App Online", + device_class=DEVICE_CLASS_CONNECTIVITY, + ), + BinarySensorEntityDescription( + key="isCharging", + name="Charging", + device_class=DEVICE_CLASS_BATTERY_CHARGING, + ), + BinarySensorEntityDescription( + key="inputDetected", + name="Input Detected", + device_class=DEVICE_CLASS_POWER, + ), +) -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 the Goal Zero Yeti sensor.""" name = entry.data[CONF_NAME] goalzero_data = hass.data[DOMAIN][entry.entry_id] @@ -17,10 +54,10 @@ async def async_setup_entry(hass, entry, async_add_entities): goalzero_data[DATA_KEY_API], goalzero_data[DATA_KEY_COORDINATOR], name, - sensor_name, + description, entry.entry_id, ) - for sensor_name in BINARY_SENSOR_DICT + for description in BINARY_SENSOR_TYPES ) @@ -29,26 +66,19 @@ class YetiBinarySensor(YetiEntity, BinarySensorEntity): def __init__( self, - api, - coordinator, - name, - sensor_name, - server_unique_id, - ): + api: Yeti, + coordinator: DataUpdateCoordinator, + name: str, + description: BinarySensorEntityDescription, + server_unique_id: str, + ) -> None: """Initialize a Goal Zero Yeti sensor.""" super().__init__(api, coordinator, name, server_unique_id) - - self._condition = sensor_name - self._attr_device_class = BINARY_SENSOR_DICT[sensor_name].get(ATTR_DEVICE_CLASS) - self._attr_icon = BINARY_SENSOR_DICT[sensor_name].get(ATTR_ICON) - self._attr_name = f"{name} {BINARY_SENSOR_DICT[sensor_name].get(ATTR_NAME)}" - self._attr_unique_id = ( - f"{server_unique_id}/{BINARY_SENSOR_DICT[sensor_name].get(ATTR_NAME)}" - ) + self.entity_description = description + self._attr_name = f"{name} {description.name}" + self._attr_unique_id = f"{server_unique_id}/{description.key}" @property def is_on(self) -> bool: """Return if the service is on.""" - if self.api.data: - return self.api.data[self._condition] == 1 - return False + return self.api.data.get(self.entity_description.key) == 1 diff --git a/homeassistant/components/goalzero/config_flow.py b/homeassistant/components/goalzero/config_flow.py index 4c525de9c7d..cc2c4a9874f 100644 --- a/homeassistant/components/goalzero/config_flow.py +++ b/homeassistant/components/goalzero/config_flow.py @@ -13,6 +13,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.typing import DiscoveryInfoType from .const import DEFAULT_NAME, DOMAIN @@ -24,11 +25,11 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize a Goal Zero Yeti flow.""" self.ip_address = None - async def async_step_dhcp(self, discovery_info): + async def async_step_dhcp(self, discovery_info: DiscoveryInfoType) -> FlowResult: """Handle dhcp discovery.""" self.ip_address = discovery_info[IP_ADDRESS] @@ -36,7 +37,7 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured(updates={CONF_HOST: self.ip_address}) self._async_abort_entries_match({CONF_HOST: self.ip_address}) - _, error = await self._async_try_connect(self.ip_address) + _, error = await self._async_try_connect(str(self.ip_address)) if error is None: return await self.async_step_confirm_discovery() return self.async_abort(reason=error) @@ -63,7 +64,9 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_step_user(self, user_input=None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by the user.""" errors = {} if user_input is not None: @@ -74,7 +77,7 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): mac_address, error = await self._async_try_connect(host) if error is None: - await self.async_set_unique_id(format_mac(mac_address)) + await self.async_set_unique_id(format_mac(str(mac_address))) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) return self.async_create_entry( title=name, @@ -98,7 +101,7 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def _async_try_connect(self, host) -> tuple: + async def _async_try_connect(self, host: str) -> tuple[str | None, str | None]: """Try connecting to Goal Zero Yeti.""" try: session = async_get_clientsession(self.hass) diff --git a/homeassistant/components/goalzero/const.py b/homeassistant/components/goalzero/const.py index e9fed7dc52b..d99cacb253e 100644 --- a/homeassistant/components/goalzero/const.py +++ b/homeassistant/components/goalzero/const.py @@ -1,37 +1,6 @@ """Constants for the Goal Zero Yeti integration.""" from datetime import timedelta -from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY_CHARGING, - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_POWER, -) -from homeassistant.components.sensor import ( - ATTR_STATE_CLASS, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, -) -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ICON, - ATTR_NAME, - ATTR_UNIT_OF_MEASUREMENT, - ELECTRIC_CURRENT_AMPERE, - ELECTRIC_POTENTIAL_VOLT, - ENERGY_WATT_HOUR, - PERCENTAGE, - POWER_WATT, - SIGNAL_STRENGTH_DECIBELS, - TEMP_CELSIUS, - TIME_MINUTES, - TIME_SECONDS, -) - ATTRIBUTION = "Data provided by Goal Zero" ATTR_DEFAULT_ENABLED = "default_enabled" @@ -41,113 +10,3 @@ DEFAULT_NAME = "Yeti" DATA_KEY_API = "api" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) - -BINARY_SENSOR_DICT = { - "backlight": {ATTR_NAME: "Backlight", ATTR_ICON: "mdi:clock-digital"}, - "app_online": { - ATTR_NAME: "App Online", - ATTR_DEVICE_CLASS: DEVICE_CLASS_CONNECTIVITY, - }, - "isCharging": { - ATTR_NAME: "Charging", - ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING, - }, - "inputDetected": { - ATTR_NAME: "Input Detected", - ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - }, -} - -SENSOR_DICT = { - "wattsIn": { - ATTR_NAME: "Watts In", - ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_DEFAULT_ENABLED: True, - }, - "ampsIn": { - ATTR_NAME: "Amps In", - ATTR_DEVICE_CLASS: DEVICE_CLASS_CURRENT, - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_DEFAULT_ENABLED: False, - }, - "wattsOut": { - ATTR_NAME: "Watts Out", - ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_DEFAULT_ENABLED: True, - }, - "ampsOut": { - ATTR_NAME: "Amps Out", - ATTR_DEVICE_CLASS: DEVICE_CLASS_CURRENT, - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_DEFAULT_ENABLED: False, - }, - "whOut": { - ATTR_NAME: "WH Out", - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_UNIT_OF_MEASUREMENT: ENERGY_WATT_HOUR, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_DEFAULT_ENABLED: False, - }, - "whStored": { - ATTR_NAME: "WH Stored", - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_UNIT_OF_MEASUREMENT: ENERGY_WATT_HOUR, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_DEFAULT_ENABLED: True, - }, - "volts": { - ATTR_NAME: "Volts", - ATTR_DEVICE_CLASS: DEVICE_CLASS_VOLTAGE, - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, - ATTR_DEFAULT_ENABLED: False, - }, - "socPercent": { - ATTR_NAME: "State of Charge Percent", - ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - ATTR_DEFAULT_ENABLED: True, - }, - "timeToEmptyFull": { - ATTR_NAME: "Time to Empty/Full", - ATTR_DEVICE_CLASS: TIME_MINUTES, - ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, - ATTR_DEFAULT_ENABLED: True, - }, - "temperature": { - ATTR_NAME: "Temperature", - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, - ATTR_DEFAULT_ENABLED: True, - }, - "wifiStrength": { - ATTR_NAME: "Wifi Strength", - ATTR_DEVICE_CLASS: DEVICE_CLASS_SIGNAL_STRENGTH, - ATTR_UNIT_OF_MEASUREMENT: SIGNAL_STRENGTH_DECIBELS, - ATTR_DEFAULT_ENABLED: True, - }, - "timestamp": { - ATTR_NAME: "Total Run Time", - ATTR_UNIT_OF_MEASUREMENT: TIME_SECONDS, - ATTR_DEFAULT_ENABLED: False, - }, - "ssid": { - ATTR_NAME: "Wi-Fi SSID", - ATTR_DEFAULT_ENABLED: False, - }, - "ipAddr": { - ATTR_NAME: "IP Address", - ATTR_DEFAULT_ENABLED: False, - }, -} - -SWITCH_DICT = { - "v12PortStatus": "12V Port Status", - "usbPortStatus": "USB Port Status", - "acPortStatus": "AC Port Status", -} diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py index dbb85aa2d48..8890c7db69c 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -2,28 +2,137 @@ from __future__ import annotations from homeassistant.components.sensor import ( - ATTR_LAST_RESET, - ATTR_STATE_CLASS, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, + SensorEntityDescription, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_NAME, - ATTR_UNIT_OF_MEASUREMENT, 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, + PERCENTAGE, + POWER_WATT, + SIGNAL_STRENGTH_DECIBELS, + TEMP_CELSIUS, + TIME_MINUTES, + TIME_SECONDS, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import YetiEntity -from .const import ( - ATTR_DEFAULT_ENABLED, - DATA_KEY_API, - DATA_KEY_COORDINATOR, - DOMAIN, - SENSOR_DICT, +from . import Yeti, YetiEntity +from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN + +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="wattsIn", + name="Watts In", + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=POWER_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="ampsIn", + name="Amps In", + device_class=DEVICE_CLASS_CURRENT, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="wattsOut", + name="Watts Out", + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=POWER_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="ampsOut", + name="Amps Out", + device_class=DEVICE_CLASS_CURRENT, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="whOut", + name="WH Out", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="whStored", + name="WH Stored", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_WATT_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="volts", + name="Volts", + device_class=DEVICE_CLASS_VOLTAGE, + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="socPercent", + name="State of Charge Percent", + device_class=DEVICE_CLASS_BATTERY, + unit_of_measurement=PERCENTAGE, + ), + SensorEntityDescription( + key="timeToEmptyFull", + name="Time to Empty/Full", + device_class=TIME_MINUTES, + unit_of_measurement=TIME_MINUTES, + ), + SensorEntityDescription( + key="temperature", + name="Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + unit_of_measurement=TEMP_CELSIUS, + ), + SensorEntityDescription( + key="wifiStrength", + name="Wifi Strength", + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + ), + SensorEntityDescription( + key="timestamp", + name="Total Run Time", + unit_of_measurement=TIME_SECONDS, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="ssid", + name="Wi-Fi SSID", + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="ipAddr", + name="IP Address", + entity_registry_enabled_default=False, + ), ) -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 the Goal Zero Yeti sensor.""" name = entry.data[CONF_NAME] goalzero_data = hass.data[DOMAIN][entry.entry_id] @@ -32,10 +141,10 @@ async def async_setup_entry(hass, entry, async_add_entities): goalzero_data[DATA_KEY_API], goalzero_data[DATA_KEY_COORDINATOR], name, - sensor_name, + description, entry.entry_id, ) - for sensor_name in SENSOR_DICT + for description in SENSOR_TYPES ] async_add_entities(sensors, True) @@ -43,22 +152,21 @@ async def async_setup_entry(hass, entry, async_add_entities): class YetiSensor(YetiEntity, SensorEntity): """Representation of a Goal Zero Yeti sensor.""" - def __init__(self, api, coordinator, name, sensor_name, server_unique_id): + def __init__( + self, + api: Yeti, + coordinator: DataUpdateCoordinator, + name: str, + description: SensorEntityDescription, + server_unique_id: str, + ) -> None: """Initialize a Goal Zero Yeti sensor.""" super().__init__(api, coordinator, name, server_unique_id) - self._condition = sensor_name - sensor = SENSOR_DICT[sensor_name] - self._attr_device_class = sensor.get(ATTR_DEVICE_CLASS) - self._attr_entity_registry_enabled_default = sensor.get(ATTR_DEFAULT_ENABLED) - self._attr_last_reset = sensor.get(ATTR_LAST_RESET) - self._attr_name = f"{name} {sensor.get(ATTR_NAME)}" - self._attr_native_unit_of_measurement = sensor.get(ATTR_UNIT_OF_MEASUREMENT) - self._attr_state_class = sensor.get(ATTR_STATE_CLASS) - self._attr_unique_id = f"{server_unique_id}/{sensor_name}" + self._attr_name = f"{name} {description.name}" + self.entity_description = description + self._attr_unique_id = f"{server_unique_id}/{description.key}" @property - def native_value(self) -> str | None: + def native_value(self) -> str: """Return the state.""" - if self.api.data: - return self.api.data.get(self._condition) - return None + return self.api.data.get(self.entity_description.key) diff --git a/homeassistant/components/goalzero/switch.py b/homeassistant/components/goalzero/switch.py index 9d37bcb0b7b..767c728e62b 100644 --- a/homeassistant/components/goalzero/switch.py +++ b/homeassistant/components/goalzero/switch.py @@ -1,14 +1,35 @@ """Support for Goal Zero Yeti Switches.""" from __future__ import annotations -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import YetiEntity -from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN, SWITCH_DICT +from . import Yeti, YetiEntity +from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN + +SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( + SwitchEntityDescription( + key="v12PortStatus", + name="12V Port Status", + ), + SwitchEntityDescription( + key="usbPortStatus", + name="USB Port Status", + ), + SwitchEntityDescription( + key="acPortStatus", + name="AC Port Status", + ), +) -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 the Goal Zero Yeti switch.""" name = entry.data[CONF_NAME] goalzero_data = hass.data[DOMAIN][entry.entry_id] @@ -17,10 +38,10 @@ async def async_setup_entry(hass, entry, async_add_entities): goalzero_data[DATA_KEY_API], goalzero_data[DATA_KEY_COORDINATOR], name, - switch_name, + description, entry.entry_id, ) - for switch_name in SWITCH_DICT + for description in SWITCH_TYPES ) @@ -29,33 +50,31 @@ class YetiSwitch(YetiEntity, SwitchEntity): def __init__( self, - api, - coordinator, - name, - switch_name, - server_unique_id, - ): + api: Yeti, + coordinator: DataUpdateCoordinator, + name: str, + description: SwitchEntityDescription, + server_unique_id: str, + ) -> None: """Initialize a Goal Zero Yeti switch.""" super().__init__(api, coordinator, name, server_unique_id) - self._condition = switch_name - self._attr_name = f"{name} {SWITCH_DICT[switch_name]}" - self._attr_unique_id = f"{server_unique_id}/{switch_name}" + self.entity_description = description + self._attr_name = f"{name} {description.name}" + self._attr_unique_id = f"{server_unique_id}/{description.key}" @property def is_on(self) -> bool: """Return state of the switch.""" - if self.api.data: - return self.api.data[self._condition] - return False + return self.api.data.get(self.entity_description.key) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn off the switch.""" - payload = {self._condition: 0} + payload = {self.entity_description.key: 0} await self.api.post_state(payload=payload) self.coordinator.async_set_updated_data(data=payload) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn on the switch.""" - payload = {self._condition: 1} + payload = {self.entity_description.key: 1} await self.api.post_state(payload=payload) self.coordinator.async_set_updated_data(data=payload) From 71b123845cc03645fecd5a030b54afc5b8c4d452 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Wed, 18 Aug 2021 22:17:16 -0700 Subject: [PATCH 351/355] Always mock SubscriptionRegistry & DiscoveryResponder for wemo tests (#53967) * Always mock SubscriptionRegistry & DiscoveryResponder for wemo tests * Use autospec=True for patch --- tests/components/wemo/conftest.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py index 7766fe512cc..bf69318706c 100644 --- a/tests/components/wemo/conftest.py +++ b/tests/components/wemo/conftest.py @@ -22,8 +22,8 @@ def pywemo_model_fixture(): return "LightSwitch" -@pytest.fixture(name="pywemo_registry") -def pywemo_registry_fixture(): +@pytest.fixture(name="pywemo_registry", autouse=True) +async def async_pywemo_registry_fixture(): """Fixture for SubscriptionRegistry instances.""" registry = create_autospec(pywemo.SubscriptionRegistry, instance=True) @@ -40,6 +40,13 @@ def pywemo_registry_fixture(): yield registry +@pytest.fixture(name="pywemo_discovery_responder", autouse=True) +def pywemo_discovery_responder_fixture(): + """Fixture for the DiscoveryResponder instance.""" + with patch("pywemo.ssdp.DiscoveryResponder", autospec=True): + yield + + @pytest.fixture(name="pywemo_device") def pywemo_device_fixture(pywemo_registry, pywemo_model): """Fixture for WeMoDevice instances.""" From e7fa3e727b1cce2f7c7764810bf2c88a8024e31b Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Wed, 18 Aug 2021 23:38:52 -0700 Subject: [PATCH 352/355] Bump pywemo to 0.6.7 (#54862) --- homeassistant/components/wemo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 21a7760741a..59eae24c714 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.6"], + "requirements": ["pywemo==0.6.7"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index 37377856e5f..96b6d6757b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1980,7 +1980,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.6.6 +pywemo==0.6.7 # homeassistant.components.wilight pywilight==0.0.70 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 48151c9392f..c7ac2def786 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1106,7 +1106,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.6.6 +pywemo==0.6.7 # homeassistant.components.wilight pywilight==0.0.70 From faec82ae8f82bf5f9fb43a3c601a2e295b49dfc8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 Aug 2021 09:27:43 +0200 Subject: [PATCH 353/355] Add binary sensor platform to Renault integration (#54750) * Add binary sensor platform * Add tests * Simplify code * Adjust descriptions * Adjust tests * Make "fuel" tests more explicit * Updates for device registry checks --- .../components/renault/binary_sensor.py | 58 +++++++ homeassistant/components/renault/const.py | 1 + tests/components/renault/const.py | 50 ++++++ .../components/renault/test_binary_sensor.py | 155 ++++++++++++++++++ 4 files changed, 264 insertions(+) create mode 100644 homeassistant/components/renault/binary_sensor.py create mode 100644 tests/components/renault/test_binary_sensor.py diff --git a/homeassistant/components/renault/binary_sensor.py b/homeassistant/components/renault/binary_sensor.py new file mode 100644 index 00000000000..dd3ccb036e0 --- /dev/null +++ b/homeassistant/components/renault/binary_sensor.py @@ -0,0 +1,58 @@ +"""Support for Renault binary sensors.""" +from __future__ import annotations + +from renault_api.kamereon.enums import ChargeState, PlugState + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY_CHARGING, + DEVICE_CLASS_PLUG, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .renault_entities import RenaultBatteryDataEntity, RenaultDataEntity +from .renault_hub import RenaultHub + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Renault entities from config entry.""" + proxy: RenaultHub = hass.data[DOMAIN][config_entry.entry_id] + entities: list[RenaultDataEntity] = [] + for vehicle in proxy.vehicles.values(): + if "battery" in vehicle.coordinators: + entities.append(RenaultPluggedInSensor(vehicle, "Plugged In")) + entities.append(RenaultChargingSensor(vehicle, "Charging")) + async_add_entities(entities) + + +class RenaultPluggedInSensor(RenaultBatteryDataEntity, BinarySensorEntity): + """Plugged In binary sensor.""" + + _attr_device_class = DEVICE_CLASS_PLUG + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + if (not self.data) or (self.data.plugStatus is None): + return None + return self.data.get_plug_status() == PlugState.PLUGGED + + +class RenaultChargingSensor(RenaultBatteryDataEntity, BinarySensorEntity): + """Charging binary sensor.""" + + _attr_device_class = DEVICE_CLASS_BATTERY_CHARGING + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + if (not self.data) or (self.data.chargingStatus is None): + return None + return self.data.get_charging_status() == ChargeState.CHARGE_IN_PROGRESS diff --git a/homeassistant/components/renault/const.py b/homeassistant/components/renault/const.py index 51f6c10c6f1..0987d1829ed 100644 --- a/homeassistant/components/renault/const.py +++ b/homeassistant/components/renault/const.py @@ -7,6 +7,7 @@ CONF_KAMEREON_ACCOUNT_ID = "kamereon_account_id" DEFAULT_SCAN_INTERVAL = 300 # 5 minutes PLATFORMS = [ + "binary_sensor", "sensor", ] diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index 8c3d6e9f98f..2c742aa07cd 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -1,4 +1,9 @@ """Constants for the Renault integration tests.""" +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY_CHARGING, + DEVICE_CLASS_PLUG, + DOMAIN as BINARY_SENSOR_DOMAIN, +) from homeassistant.components.renault.const import ( CONF_KAMEREON_ACCOUNT_ID, CONF_LOCALE, @@ -19,6 +24,8 @@ from homeassistant.const import ( LENGTH_KILOMETERS, PERCENTAGE, POWER_KILO_WATT, + STATE_OFF, + STATE_ON, STATE_UNKNOWN, TEMP_CELSIUS, TIME_MINUTES, @@ -54,6 +61,20 @@ MOCK_VEHICLES = { "cockpit": "cockpit_ev.json", "hvac_status": "hvac_status.json", }, + BINARY_SENSOR_DOMAIN: [ + { + "entity_id": "binary_sensor.plugged_in", + "unique_id": "vf1aaaaa555777999_plugged_in", + "result": STATE_ON, + "class": DEVICE_CLASS_PLUG, + }, + { + "entity_id": "binary_sensor.charging", + "unique_id": "vf1aaaaa555777999_charging", + "result": STATE_ON, + "class": DEVICE_CLASS_BATTERY_CHARGING, + }, + ], SENSOR_DOMAIN: [ { "entity_id": "sensor.battery_autonomy", @@ -147,6 +168,20 @@ MOCK_VEHICLES = { "charge_mode": "charge_mode_schedule.json", "cockpit": "cockpit_ev.json", }, + BINARY_SENSOR_DOMAIN: [ + { + "entity_id": "binary_sensor.plugged_in", + "unique_id": "vf1aaaaa555777999_plugged_in", + "result": STATE_OFF, + "class": DEVICE_CLASS_PLUG, + }, + { + "entity_id": "binary_sensor.charging", + "unique_id": "vf1aaaaa555777999_charging", + "result": STATE_OFF, + "class": DEVICE_CLASS_BATTERY_CHARGING, + }, + ], SENSOR_DOMAIN: [ { "entity_id": "sensor.battery_autonomy", @@ -233,6 +268,20 @@ MOCK_VEHICLES = { "charge_mode": "charge_mode_always.json", "cockpit": "cockpit_fuel.json", }, + BINARY_SENSOR_DOMAIN: [ + { + "entity_id": "binary_sensor.plugged_in", + "unique_id": "vf1aaaaa555777123_plugged_in", + "result": STATE_ON, + "class": DEVICE_CLASS_PLUG, + }, + { + "entity_id": "binary_sensor.charging", + "unique_id": "vf1aaaaa555777123_charging", + "result": STATE_ON, + "class": DEVICE_CLASS_BATTERY_CHARGING, + }, + ], SENSOR_DOMAIN: [ { "entity_id": "sensor.battery_autonomy", @@ -327,6 +376,7 @@ MOCK_VEHICLES = { # Ignore, # charge-mode ], "endpoints": {"cockpit": "cockpit_fuel.json"}, + BINARY_SENSOR_DOMAIN: [], SENSOR_DOMAIN: [ { "entity_id": "sensor.fuel_autonomy", diff --git a/tests/components/renault/test_binary_sensor.py b/tests/components/renault/test_binary_sensor.py new file mode 100644 index 00000000000..71bb90f16a6 --- /dev/null +++ b/tests/components/renault/test_binary_sensor.py @@ -0,0 +1,155 @@ +"""Tests for Renault binary sensors.""" +from unittest.mock import patch + +import pytest +from renault_api.kamereon import exceptions + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE +from homeassistant.setup import async_setup_component + +from . import ( + check_device_registry, + setup_renault_integration_vehicle, + setup_renault_integration_vehicle_with_no_data, + setup_renault_integration_vehicle_with_side_effect, +) +from .const import MOCK_VEHICLES + +from tests.common import mock_device_registry, mock_registry + + +@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) +async def test_binary_sensors(hass, vehicle_type): + """Test for Renault binary sensors.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + with patch("homeassistant.components.renault.PLATFORMS", [BINARY_SENSOR_DOMAIN]): + await setup_renault_integration_vehicle(hass, vehicle_type) + await hass.async_block_till_done() + + mock_vehicle = MOCK_VEHICLES[vehicle_type] + check_device_registry(device_registry, mock_vehicle["expected_device"]) + + expected_entities = mock_vehicle[BINARY_SENSOR_DOMAIN] + assert len(entity_registry.entities) == len(expected_entities) + for expected_entity in expected_entities: + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_entity["unique_id"] + assert registry_entry.unit_of_measurement == expected_entity.get("unit") + assert registry_entry.device_class == expected_entity.get("class") + state = hass.states.get(entity_id) + assert state.state == expected_entity["result"] + + +@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) +async def test_binary_sensor_empty(hass, vehicle_type): + """Test for Renault binary sensors with empty data from Renault.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + with patch("homeassistant.components.renault.PLATFORMS", [BINARY_SENSOR_DOMAIN]): + await setup_renault_integration_vehicle_with_no_data(hass, vehicle_type) + await hass.async_block_till_done() + + mock_vehicle = MOCK_VEHICLES[vehicle_type] + check_device_registry(device_registry, mock_vehicle["expected_device"]) + + expected_entities = mock_vehicle[BINARY_SENSOR_DOMAIN] + assert len(entity_registry.entities) == len(expected_entities) + for expected_entity in expected_entities: + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_entity["unique_id"] + assert registry_entry.unit_of_measurement == expected_entity.get("unit") + assert registry_entry.device_class == expected_entity.get("class") + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + +@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) +async def test_binary_sensor_errors(hass, vehicle_type): + """Test for Renault binary sensors with temporary failure.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + invalid_upstream_exception = exceptions.InvalidUpstreamException( + "err.tech.500", + "Invalid response from the upstream server (The request sent to the GDC is erroneous) ; 502 Bad Gateway", + ) + + with patch("homeassistant.components.renault.PLATFORMS", [BINARY_SENSOR_DOMAIN]): + await setup_renault_integration_vehicle_with_side_effect( + hass, vehicle_type, invalid_upstream_exception + ) + await hass.async_block_till_done() + + mock_vehicle = MOCK_VEHICLES[vehicle_type] + check_device_registry(device_registry, mock_vehicle["expected_device"]) + + expected_entities = mock_vehicle[BINARY_SENSOR_DOMAIN] + assert len(entity_registry.entities) == len(expected_entities) + for expected_entity in expected_entities: + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_entity["unique_id"] + assert registry_entry.unit_of_measurement == expected_entity.get("unit") + assert registry_entry.device_class == expected_entity.get("class") + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + + +async def test_binary_sensor_access_denied(hass): + """Test for Renault binary sensors with access denied failure.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + vehicle_type = "zoe_40" + access_denied_exception = exceptions.AccessDeniedException( + "err.func.403", + "Access is denied for this resource", + ) + + with patch("homeassistant.components.renault.PLATFORMS", [BINARY_SENSOR_DOMAIN]): + await setup_renault_integration_vehicle_with_side_effect( + hass, vehicle_type, access_denied_exception + ) + await hass.async_block_till_done() + + mock_vehicle = MOCK_VEHICLES[vehicle_type] + check_device_registry(device_registry, mock_vehicle["expected_device"]) + + assert len(entity_registry.entities) == 0 + + +async def test_binary_sensor_not_supported(hass): + """Test for Renault binary sensors with not supported failure.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + vehicle_type = "zoe_40" + not_supported_exception = exceptions.NotSupportedException( + "err.tech.501", + "This feature is not technically supported by this gateway", + ) + + with patch("homeassistant.components.renault.PLATFORMS", [BINARY_SENSOR_DOMAIN]): + await setup_renault_integration_vehicle_with_side_effect( + hass, vehicle_type, not_supported_exception + ) + await hass.async_block_till_done() + + mock_vehicle = MOCK_VEHICLES[vehicle_type] + check_device_registry(device_registry, mock_vehicle["expected_device"]) + + assert len(entity_registry.entities) == 0 From 0688aaa2b6546feae6241410541aae1675a4f0c0 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 19 Aug 2021 09:37:31 +0200 Subject: [PATCH 354/355] Check for duplicate entity name/address in modbus entities (#54669) * Check for duplicate entity name/address. --- homeassistant/components/modbus/__init__.py | 8 ++++- homeassistant/components/modbus/validators.py | 34 +++++++++++++++++++ tests/components/modbus/test_cover.py | 2 +- tests/components/modbus/test_fan.py | 2 +- tests/components/modbus/test_light.py | 2 +- tests/components/modbus/test_switch.py | 2 +- 6 files changed, 45 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 12e2273bf88..e98a61257c6 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -116,7 +116,12 @@ from .const import ( UDP, ) from .modbus import ModbusHub, async_modbus_setup -from .validators import number_validator, scan_interval_validator, struct_validator +from .validators import ( + duplicate_entity_validator, + number_validator, + scan_interval_validator, + struct_validator, +) _LOGGER = logging.getLogger(__name__) @@ -327,6 +332,7 @@ CONFIG_SCHEMA = vol.Schema( DOMAIN: vol.All( cv.ensure_list, scan_interval_validator, + duplicate_entity_validator, [ vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA), ], diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index b59557e58d2..543618e11fd 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -9,9 +9,11 @@ from typing import Any import voluptuous as vol from homeassistant.const import ( + CONF_ADDRESS, CONF_COUNT, CONF_NAME, CONF_SCAN_INTERVAL, + CONF_SLAVE, CONF_STRUCTURE, CONF_TIMEOUT, ) @@ -189,3 +191,35 @@ def scan_interval_validator(config: dict) -> dict: ) hub[CONF_TIMEOUT] = minimum_scan_interval - 1 return config + + +def duplicate_entity_validator(config: dict) -> dict: + """Control scan_interval.""" + for hub_index, hub in enumerate(config): + addresses: set[str] = set() + for component, conf_key in PLATFORMS: + if conf_key not in hub: + continue + names: set[str] = set() + errors: list[int] = [] + for index, entry in enumerate(hub[conf_key]): + name = entry[CONF_NAME] + addr = str(entry[CONF_ADDRESS]) + if CONF_SLAVE in entry: + addr += "_" + str(entry[CONF_SLAVE]) + if addr in addresses: + err = f"Modbus {component}/{name} address {addr} is duplicate, second entry not loaded!" + _LOGGER.warning(err) + errors.append(index) + elif name in names: + err = f"Modbus {component}/{name}  is duplicate, second entry not loaded!" + _LOGGER.warning(err) + errors.append(index) + else: + names.add(name) + addresses.add(addr) + + for i in reversed(errors): + del config[hub_index][conf_key][i] + + return config diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py index 266193294c6..a315d8176ae 100644 --- a/tests/components/modbus/test_cover.py +++ b/tests/components/modbus/test_cover.py @@ -235,7 +235,7 @@ async def test_restore_state_cover(hass, mock_test_state, mock_modbus): { CONF_NAME: f"{TEST_ENTITY_NAME}2", CONF_INPUT_TYPE: CALL_TYPE_COIL, - CONF_ADDRESS: 1234, + CONF_ADDRESS: 1235, CONF_SCAN_INTERVAL: 0, }, ] diff --git a/tests/components/modbus/test_fan.py b/tests/components/modbus/test_fan.py index fb65f737d27..821a5cace99 100644 --- a/tests/components/modbus/test_fan.py +++ b/tests/components/modbus/test_fan.py @@ -231,7 +231,7 @@ async def test_fan_service_turn(hass, caplog, mock_pymodbus): }, { CONF_NAME: f"{TEST_ENTITY_NAME}2", - CONF_ADDRESS: 17, + CONF_ADDRESS: 18, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SCAN_INTERVAL: 0, CONF_VERIFY: {}, diff --git a/tests/components/modbus/test_light.py b/tests/components/modbus/test_light.py index f679883e908..486dfdc64f8 100644 --- a/tests/components/modbus/test_light.py +++ b/tests/components/modbus/test_light.py @@ -231,7 +231,7 @@ async def test_light_service_turn(hass, caplog, mock_pymodbus): }, { CONF_NAME: f"{TEST_ENTITY_NAME}2", - CONF_ADDRESS: 17, + CONF_ADDRESS: 18, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SCAN_INTERVAL: 0, CONF_VERIFY: {}, diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py index 302189001c5..fb929d26caf 100644 --- a/tests/components/modbus/test_switch.py +++ b/tests/components/modbus/test_switch.py @@ -245,7 +245,7 @@ async def test_switch_service_turn(hass, caplog, mock_pymodbus): }, { CONF_NAME: f"{TEST_ENTITY_NAME}2", - CONF_ADDRESS: 17, + CONF_ADDRESS: 18, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SCAN_INTERVAL: 0, CONF_VERIFY: {}, From 32a2c5d5dbdeacbeeb58e62458bbb40bee07d945 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 19 Aug 2021 10:11:20 +0200 Subject: [PATCH 355/355] Add support for Swedish smart electricity meters to DSMR (#54630) * Add support for Swedish smart electricity meters to DSMR * Use Swedish protocol support from dsmr_parser * Update tests * Bump dsmr_parser to 0.30 * Remove last_reset attribute from Swedish energy sensors --- homeassistant/components/dsmr/config_flow.py | 21 ++++--- homeassistant/components/dsmr/const.py | 31 ++++++++++ homeassistant/components/dsmr/manifest.json | 2 +- homeassistant/components/dsmr/sensor.py | 9 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/dsmr/conftest.py | 6 ++ tests/components/dsmr/test_config_flow.py | 24 ++++++++ tests/components/dsmr/test_sensor.py | 65 ++++++++++++++++++++ 9 files changed, 149 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index 72e854fe43a..9670aab21cf 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -28,6 +28,7 @@ from .const import ( CONF_TIME_BETWEEN_UPDATE, DEFAULT_TIME_BETWEEN_UPDATE, DOMAIN, + DSMR_VERSIONS, LOGGER, ) @@ -70,6 +71,10 @@ class DSMRConnection: if self._equipment_identifier in telegram: self._telegram = telegram transport.close() + # Swedish meters have no equipment identifier + if self._dsmr_version == "5S" and obis_ref.P1_MESSAGE_TIMESTAMP in telegram: + self._telegram = telegram + transport.close() if self._host is None: reader_factory = partial( @@ -119,7 +124,7 @@ async def _validate_dsmr_connection( equipment_identifier_gas = conn.equipment_identifier_gas() # Check only for equipment identifier in case no gas meter is connected - if equipment_identifier is None: + if equipment_identifier is None and data[CONF_DSMR_VERSION] != "5S": raise CannotCommunicate return { @@ -203,7 +208,7 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): { vol.Required(CONF_HOST): str, vol.Required(CONF_PORT): int, - vol.Required(CONF_DSMR_VERSION): vol.In(["2.2", "4", "5", "5B", "5L"]), + vol.Required(CONF_DSMR_VERSION): vol.In(DSMR_VERSIONS), } ) return self.async_show_form( @@ -247,7 +252,7 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): schema = vol.Schema( { vol.Required(CONF_PORT): vol.In(list_of_ports), - vol.Required(CONF_DSMR_VERSION): vol.In(["2.2", "4", "5", "5B", "5L"]), + vol.Required(CONF_DSMR_VERSION): vol.In(DSMR_VERSIONS), } ) return self.async_show_form( @@ -288,8 +293,9 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data = {**data, **info} - await self.async_set_unique_id(info[CONF_SERIAL_ID]) - self._abort_if_unique_id_configured() + if info[CONF_SERIAL_ID]: + await self.async_set_unique_id(info[CONF_SERIAL_ID]) + self._abort_if_unique_id_configured() except CannotConnect: errors["base"] = "cannot_connect" except CannotCommunicate: @@ -316,8 +322,9 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): name = f"{host}:{port}" if host is not None else port data = {**import_config, **info} - await self.async_set_unique_id(info[CONF_SERIAL_ID]) - self._abort_if_unique_id_configured(data) + 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) diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index 6c392526ee3..ba90fa9b697 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -44,6 +44,8 @@ DATA_TASK = "task" DEVICE_NAME_ENERGY = "Energy Meter" DEVICE_NAME_GAS = "Gas Meter" +DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S"} + SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key=obis_references.CURRENT_ELECTRICITY_USAGE, @@ -62,11 +64,13 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key=obis_references.ELECTRICITY_ACTIVE_TARIFF, name="Power Tariff", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, icon="mdi:flash", ), DSMRSensorEntityDescription( key=obis_references.ELECTRICITY_USED_TARIFF_1, name="Energy Consumption (tarif 1)", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, device_class=DEVICE_CLASS_ENERGY, force_update=True, state_class=STATE_CLASS_TOTAL_INCREASING, @@ -74,6 +78,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key=obis_references.ELECTRICITY_USED_TARIFF_2, name="Energy Consumption (tarif 2)", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, force_update=True, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, @@ -81,6 +86,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key=obis_references.ELECTRICITY_DELIVERED_TARIFF_1, name="Energy Production (tarif 1)", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, force_update=True, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, @@ -88,6 +94,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key=obis_references.ELECTRICITY_DELIVERED_TARIFF_2, name="Energy Production (tarif 2)", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, force_update=True, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, @@ -137,45 +144,53 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key=obis_references.SHORT_POWER_FAILURE_COUNT, name="Short Power Failure Count", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:flash-off", ), DSMRSensorEntityDescription( key=obis_references.LONG_POWER_FAILURE_COUNT, name="Long Power Failure Count", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:flash-off", ), 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, ), 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, ), 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, ), DSMRSensorEntityDescription( key=obis_references.VOLTAGE_SWELL_L1_COUNT, name="Voltage Swells Phase L1", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:pulse", ), DSMRSensorEntityDescription( key=obis_references.VOLTAGE_SWELL_L2_COUNT, name="Voltage Swells Phase L2", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:pulse", ), DSMRSensorEntityDescription( key=obis_references.VOLTAGE_SWELL_L3_COUNT, name="Voltage Swells Phase L3", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:pulse", ), @@ -237,6 +252,22 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), + DSMRSensorEntityDescription( + key=obis_references.SWEDEN_ELECTRICITY_USED_TARIFF_GLOBAL, + name="Energy Consumption (total)", + dsmr_versions={"5S"}, + force_update=True, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key=obis_references.SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, + name="Energy Production (total)", + dsmr_versions={"5S"}, + force_update=True, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), DSMRSensorEntityDescription( key=obis_references.ELECTRICITY_IMPORTED_TOTAL, name="Energy Consumption (total)", diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index df738724ac0..fbbfac55959 100644 --- a/homeassistant/components/dsmr/manifest.json +++ b/homeassistant/components/dsmr/manifest.json @@ -2,7 +2,7 @@ "domain": "dsmr", "name": "DSMR Slimme Meter", "documentation": "https://www.home-assistant.io/integrations/dsmr", - "requirements": ["dsmr_parser==0.29"], + "requirements": ["dsmr_parser==0.30"], "codeowners": ["@Robbie1221", "@frenck"], "config_flow": true, "iot_class": "local_push" diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index dbc29144719..1b38b2695ec 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -44,6 +44,7 @@ from .const import ( DEVICE_NAME_ENERGY, DEVICE_NAME_GAS, DOMAIN, + DSMR_VERSIONS, LOGGER, SENSORS, ) @@ -54,7 +55,7 @@ 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(["5L", "5B", "5", "4", "2.2"]) + 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), @@ -118,7 +119,7 @@ async def async_setup_entry( create_tcp_dsmr_reader, entry.data[CONF_HOST], entry.data[CONF_PORT], - entry.data[CONF_DSMR_VERSION], + dsmr_version, update_entities_telegram, loop=hass.loop, keep_alive_interval=60, @@ -127,7 +128,7 @@ async def async_setup_entry( reader_factory = partial( create_dsmr_reader, entry.data[CONF_PORT], - entry.data[CONF_DSMR_VERSION], + dsmr_version, update_entities_telegram, loop=hass.loop, ) @@ -217,6 +218,8 @@ class DSMREntity(SensorEntity): if entity_description.is_gas: device_serial = entry.data[CONF_SERIAL_ID_GAS] device_name = DEVICE_NAME_GAS + if device_serial is None: + device_serial = entry.entry_id self._attr_device_info = { "identifiers": {(DOMAIN, device_serial)}, diff --git a/requirements_all.txt b/requirements_all.txt index 96b6d6757b7..d5adf797311 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -532,7 +532,7 @@ doorbirdpy==2.1.0 dovado==0.4.1 # homeassistant.components.dsmr -dsmr_parser==0.29 +dsmr_parser==0.30 # homeassistant.components.dwd_weather_warnings dwdwfsapi==1.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c7ac2def786..ef1e3ca0fc8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -307,7 +307,7 @@ directv==0.4.0 doorbirdpy==2.1.0 # homeassistant.components.dsmr -dsmr_parser==0.29 +dsmr_parser==0.30 # homeassistant.components.dynalite dynalite_devices==0.1.46 diff --git a/tests/components/dsmr/conftest.py b/tests/components/dsmr/conftest.py index ab7b3a4d479..9ef6bccfab5 100644 --- a/tests/components/dsmr/conftest.py +++ b/tests/components/dsmr/conftest.py @@ -7,6 +7,7 @@ from dsmr_parser.obis_references import ( EQUIPMENT_IDENTIFIER, EQUIPMENT_IDENTIFIER_GAS, LUXEMBOURG_EQUIPMENT_IDENTIFIER, + P1_MESSAGE_TIMESTAMP, ) from dsmr_parser.objects import CosemObject import pytest @@ -44,6 +45,7 @@ async def dsmr_connection_send_validate_fixture(hass): protocol.telegram = { EQUIPMENT_IDENTIFIER: CosemObject([{"value": "12345678", "unit": ""}]), EQUIPMENT_IDENTIFIER_GAS: CosemObject([{"value": "123456789", "unit": ""}]), + P1_MESSAGE_TIMESTAMP: CosemObject([{"value": "12345678", "unit": ""}]), } async def connection_factory(*args, **kwargs): @@ -57,6 +59,10 @@ async def dsmr_connection_send_validate_fixture(hass): [{"value": "123456789", "unit": ""}] ), } + if args[1] == "5S": + protocol.telegram = { + P1_MESSAGE_TIMESTAMP: CosemObject([{"value": "12345678", "unit": ""}]), + } return (transport, protocol) diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index 006893a81e8..d56cd3f2eb8 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -13,6 +13,7 @@ from homeassistant.components.dsmr import DOMAIN, config_flow from tests.common import MockConfigEntry SERIAL_DATA = {"serial_id": "12345678", "serial_id_gas": "123456789"} +SERIAL_DATA_SWEDEN = {"serial_id": None, "serial_id_gas": None} def com_port(): @@ -482,6 +483,29 @@ async def test_import_luxembourg(hass, dsmr_connection_send_validate_fixture): assert result["data"] == {**entry_data, **SERIAL_DATA} +async def test_import_sweden(hass, dsmr_connection_send_validate_fixture): + """Test we can import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + 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 6d40437d87a..6accf7c40da 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -536,6 +536,71 @@ async def test_belgian_meter_low(hass, dsmr_connection_fixture): assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" +async def test_swedish_meter(hass, dsmr_connection_fixture): + """Test if v5 meter is correctly parsed.""" + (connection_factory, transport, protocol) = dsmr_connection_fixture + + from dsmr_parser.obis_references import ( + SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, + SWEDEN_ELECTRICITY_USED_TARIFF_GLOBAL, + ) + from dsmr_parser.objects import CosemObject + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "5S", + "precision": 4, + "reconnect_interval": 30, + "serial_id": None, + "serial_id_gas": None, + } + entry_options = { + "time_between_update": 0, + } + + telegram = { + SWEDEN_ELECTRICITY_USED_TARIFF_GLOBAL: CosemObject( + [{"value": Decimal(123.456), "unit": ENERGY_KILO_WATT_HOUR}] + ), + SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: CosemObject( + [{"value": Decimal(654.321), "unit": ENERGY_KILO_WATT_HOUR}] + ), + } + + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data, options=entry_options + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + telegram_callback = connection_factory.call_args_list[0][0][2] + + # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser + telegram_callback(telegram) + + # after receiving telegram entities need to have the chance to update + await asyncio.sleep(0) + + power_tariff = hass.states.get("sensor.energy_consumption_total") + assert power_tariff.state == "123.456" + assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert power_tariff.attributes.get(ATTR_ICON) is None + assert power_tariff.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert ( + power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + ) + + power_tariff = hass.states.get("sensor.energy_production_total") + assert power_tariff.state == "654.321" + assert power_tariff.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert ( + power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + ) + + async def test_tcp(hass, dsmr_connection_fixture): """If proper config provided TCP connection should be made.""" (connection_factory, transport, protocol) = dsmr_connection_fixture

  • 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

    CmE^$)X$Hpq)&T7Uq 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 O7(8A5T-G@yGywoX3(_$F literal 0 HcmV?d00001 diff --git a/homeassistant/components/demo/demo_2.png b/homeassistant/components/demo/demo_2.png new file mode 100644 index 0000000000000000000000000000000000000000..97a8e49025d4a6f2b5914e1cf95773e7bace2817 GIT binary patch literal 231077 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 z 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 z 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.Q50aZ6yy(!`7<0v4tAF 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

    ZLcJv7qKc$3!S?AkL8=(VCj=T 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