From 0bf034c3318f27e649389830d4ad7a7e10eb2d6f Mon Sep 17 00:00:00 2001 From: Es Date: Thu, 20 May 2021 19:41:57 +0200 Subject: [PATCH] Isolated the async_add_executor_job to make the solution more async --- homeassistant-2021.6.0.dev0/LICENSE.md | 201 ++ homeassistant-2021.6.0.dev0/MANIFEST.in | 4 + homeassistant-2021.6.0.dev0/README.rst | 28 + .../homeassistant/__init__.py | 1 + .../homeassistant/__main__.py | 322 +++ .../homeassistant/auth/__init__.py | 557 +++++ .../homeassistant/auth/auth_store.py | 609 +++++ .../homeassistant/auth/const.py | 9 + .../auth/mfa_modules/__init__.py | 175 ++ .../auth/mfa_modules/insecure_example.py | 87 + .../homeassistant/auth/mfa_modules/notify.py | 359 +++ .../homeassistant/auth/mfa_modules/totp.py | 237 ++ .../homeassistant/auth/models.py | 135 ++ .../auth/permissions/__init__.py | 77 + .../homeassistant/auth/permissions/const.py | 8 + .../auth/permissions/entities.py | 100 + .../homeassistant/auth/permissions/merge.py | 67 + .../homeassistant/auth/permissions/models.py | 20 + .../auth/permissions/system_policies.py | 8 + .../homeassistant/auth/permissions/types.py | 28 + .../homeassistant/auth/permissions/util.py | 112 + .../homeassistant/auth/providers/__init__.py | 292 +++ .../auth/providers/command_line.py | 155 ++ .../auth/providers/homeassistant.py | 346 +++ .../auth/providers/insecure_example.py | 124 + .../auth/providers/legacy_api_password.py | 104 + .../auth/providers/trusted_networks.py | 225 ++ .../homeassistant/block_async_io.py | 14 + .../homeassistant/bootstrap.py | 580 +++++ .../homeassistant/components/__init__.py | 46 + .../components/abode/__init__.py | 409 ++++ .../components/abode/alarm_control_panel.py | 79 + .../components/abode/binary_sensor.py | 46 + .../homeassistant/components/abode/camera.py | 102 + .../components/abode/config_flow.py | 170 ++ .../homeassistant/components/abode/const.py | 9 + .../homeassistant/components/abode/cover.py | 36 + .../homeassistant/components/abode/light.py | 98 + .../homeassistant/components/abode/lock.py | 36 + .../components/abode/manifest.json | 12 + .../homeassistant/components/abode/sensor.py | 80 + .../components/abode/services.yaml | 46 + .../components/abode/strings.json | 36 + .../homeassistant/components/abode/switch.py | 80 + .../components/abode/translations/bg.json | 16 + .../components/abode/translations/ca.json | 35 + .../components/abode/translations/cs.json | 35 + .../components/abode/translations/da.json | 16 + .../components/abode/translations/de.json | 35 + .../components/abode/translations/el.json | 8 + .../components/abode/translations/en.json | 35 + .../components/abode/translations/es-419.json | 31 + .../components/abode/translations/es.json | 35 + .../components/abode/translations/et.json | 35 + .../components/abode/translations/fa.json | 12 + .../components/abode/translations/fr.json | 35 + .../components/abode/translations/he.json | 11 + .../components/abode/translations/hu.json | 34 + .../components/abode/translations/id.json | 35 + .../components/abode/translations/it.json | 35 + .../components/abode/translations/ko.json | 35 + .../components/abode/translations/lb.json | 35 + .../components/abode/translations/lv.json | 12 + .../components/abode/translations/nl.json | 35 + .../components/abode/translations/no.json | 35 + .../components/abode/translations/pl.json | 35 + .../components/abode/translations/pt-BR.json | 15 + .../components/abode/translations/pt.json | 26 + .../components/abode/translations/ro.json | 7 + .../components/abode/translations/ru.json | 35 + .../components/abode/translations/sl.json | 33 + .../components/abode/translations/sv.json | 16 + .../components/abode/translations/th.json | 9 + .../components/abode/translations/tr.json | 34 + .../components/abode/translations/uk.json | 35 + .../abode/translations/zh-Hant.json | 35 + .../components/accuweather/__init__.py | 112 + .../components/accuweather/config_flow.py | 111 + .../components/accuweather/const.py | 315 +++ .../components/accuweather/manifest.json | 10 + .../components/accuweather/sensor.py | 166 ++ .../components/accuweather/strings.json | 41 + .../accuweather/strings.sensor.json | 9 + .../components/accuweather/system_health.py | 27 + .../accuweather/translations/ca.json | 41 + .../accuweather/translations/cs.json | 41 + .../accuweather/translations/de.json | 41 + .../accuweather/translations/en.json | 41 + .../accuweather/translations/es-419.json | 29 + .../accuweather/translations/es.json | 41 + .../accuweather/translations/et.json | 41 + .../accuweather/translations/fr.json | 41 + .../accuweather/translations/he.json | 11 + .../accuweather/translations/hu.json | 29 + .../accuweather/translations/id.json | 41 + .../accuweather/translations/it.json | 41 + .../accuweather/translations/ko.json | 41 + .../accuweather/translations/lb.json | 41 + .../accuweather/translations/nl.json | 41 + .../accuweather/translations/no.json | 41 + .../accuweather/translations/pl.json | 41 + .../accuweather/translations/pt-BR.json | 24 + .../accuweather/translations/pt.json | 30 + .../accuweather/translations/ru.json | 41 + .../accuweather/translations/sensor.ca.json | 9 + .../accuweather/translations/sensor.cs.json | 9 + .../accuweather/translations/sensor.de.json | 9 + .../accuweather/translations/sensor.en.json | 9 + .../translations/sensor.es-419.json | 9 + .../accuweather/translations/sensor.es.json | 9 + .../accuweather/translations/sensor.et.json | 9 + .../accuweather/translations/sensor.fr.json | 9 + .../accuweather/translations/sensor.hu.json | 9 + .../accuweather/translations/sensor.id.json | 9 + .../accuweather/translations/sensor.it.json | 9 + .../accuweather/translations/sensor.ko.json | 9 + .../accuweather/translations/sensor.lb.json | 9 + .../accuweather/translations/sensor.nl.json | 9 + .../accuweather/translations/sensor.no.json | 9 + .../accuweather/translations/sensor.pl.json | 9 + .../accuweather/translations/sensor.ru.json | 9 + .../accuweather/translations/sensor.sv.json | 8 + .../accuweather/translations/sensor.uk.json | 9 + .../translations/sensor.zh-Hant.json | 9 + .../accuweather/translations/sl.json | 8 + .../accuweather/translations/tr.json | 38 + .../accuweather/translations/uk.json | 41 + .../accuweather/translations/zh-Hans.json | 8 + .../accuweather/translations/zh-Hant.json | 41 + .../components/accuweather/weather.py | 176 ++ .../components/acer_projector/__init__.py | 1 + .../components/acer_projector/const.py | 34 + .../components/acer_projector/manifest.json | 8 + .../components/acer_projector/switch.py | 172 ++ .../components/acmeda/__init__.py | 46 + .../homeassistant/components/acmeda/base.py | 87 + .../components/acmeda/config_flow.py | 68 + .../homeassistant/components/acmeda/const.py | 8 + .../homeassistant/components/acmeda/cover.py | 121 + .../homeassistant/components/acmeda/errors.py | 10 + .../components/acmeda/helpers.py | 40 + .../homeassistant/components/acmeda/hub.py | 89 + .../components/acmeda/manifest.json | 9 + .../homeassistant/components/acmeda/sensor.py | 47 + .../components/acmeda/strings.json | 15 + .../components/acmeda/translations/ca.json | 15 + .../components/acmeda/translations/cs.json | 14 + .../components/acmeda/translations/de.json | 15 + .../components/acmeda/translations/en.json | 15 + .../components/acmeda/translations/es.json | 15 + .../components/acmeda/translations/et.json | 15 + .../components/acmeda/translations/fr.json | 15 + .../components/acmeda/translations/hu.json | 7 + .../components/acmeda/translations/id.json | 15 + .../components/acmeda/translations/it.json | 15 + .../components/acmeda/translations/ko.json | 15 + .../components/acmeda/translations/lb.json | 15 + .../components/acmeda/translations/nl.json | 15 + .../components/acmeda/translations/no.json | 15 + .../components/acmeda/translations/pl.json | 15 + .../components/acmeda/translations/pt.json | 7 + .../components/acmeda/translations/ru.json | 15 + .../components/acmeda/translations/tr.json | 11 + .../components/acmeda/translations/uk.json | 15 + .../acmeda/translations/zh-Hant.json | 15 + .../components/actiontec/__init__.py | 1 + .../components/actiontec/const.py | 12 + .../components/actiontec/device_tracker.py | 115 + .../components/actiontec/manifest.json | 7 + .../components/actiontec/model.py | 11 + .../components/adguard/__init__.py | 208 ++ .../components/adguard/config_flow.py | 148 ++ .../homeassistant/components/adguard/const.py | 14 + .../components/adguard/manifest.json | 9 + .../components/adguard/sensor.py | 248 ++ .../components/adguard/services.yaml | 66 + .../components/adguard/strings.json | 28 + .../components/adguard/switch.py | 238 ++ .../components/adguard/translations/bg.json | 22 + .../components/adguard/translations/ca.json | 28 + .../components/adguard/translations/cs.json | 28 + .../components/adguard/translations/da.json | 22 + .../components/adguard/translations/de.json | 28 + .../components/adguard/translations/el.json | 7 + .../components/adguard/translations/en.json | 28 + .../adguard/translations/es-419.json | 24 + .../components/adguard/translations/es.json | 28 + .../components/adguard/translations/et.json | 28 + .../components/adguard/translations/fi.json | 12 + .../components/adguard/translations/fr.json | 28 + .../components/adguard/translations/he.json | 13 + .../components/adguard/translations/hr.json | 7 + .../components/adguard/translations/hu.json | 19 + .../components/adguard/translations/id.json | 27 + .../components/adguard/translations/it.json | 28 + .../components/adguard/translations/ko.json | 28 + .../components/adguard/translations/lb.json | 27 + .../components/adguard/translations/nl.json | 28 + .../components/adguard/translations/nn.json | 11 + .../components/adguard/translations/no.json | 28 + .../components/adguard/translations/pl.json | 28 + .../adguard/translations/pt-BR.json | 22 + .../components/adguard/translations/pt.json | 22 + .../components/adguard/translations/ru.json | 28 + .../components/adguard/translations/sl.json | 24 + .../components/adguard/translations/sv.json | 22 + .../components/adguard/translations/tr.json | 17 + .../components/adguard/translations/uk.json | 27 + .../components/adguard/translations/vi.json | 12 + .../adguard/translations/zh-Hans.json | 15 + .../adguard/translations/zh-Hant.json | 28 + .../homeassistant/components/ads/__init__.py | 331 +++ .../components/ads/binary_sensor.py | 57 + .../homeassistant/components/ads/cover.py | 195 ++ .../homeassistant/components/ads/light.py | 92 + .../components/ads/manifest.json | 8 + .../homeassistant/components/ads/sensor.py | 73 + .../components/ads/services.yaml | 36 + .../homeassistant/components/ads/switch.py | 48 + .../components/advantage_air/__init__.py | 72 + .../components/advantage_air/binary_sensor.py | 102 + .../components/advantage_air/climate.py | 250 ++ .../components/advantage_air/config_flow.py | 57 + .../components/advantage_air/const.py | 7 + .../components/advantage_air/cover.py | 116 + .../components/advantage_air/entity.py | 35 + .../components/advantage_air/manifest.json | 10 + .../components/advantage_air/sensor.py | 153 ++ .../components/advantage_air/services.yaml | 26 + .../components/advantage_air/strings.json | 20 + .../components/advantage_air/switch.py | 58 + .../advantage_air/translations/ca.json | 20 + .../advantage_air/translations/cs.json | 20 + .../advantage_air/translations/de.json | 20 + .../advantage_air/translations/en.json | 20 + .../advantage_air/translations/es-419.json | 9 + .../advantage_air/translations/es.json | 20 + .../advantage_air/translations/et.json | 20 + .../advantage_air/translations/fr.json | 20 + .../advantage_air/translations/hu.json | 20 + .../advantage_air/translations/id.json | 20 + .../advantage_air/translations/it.json | 20 + .../advantage_air/translations/ka.json | 17 + .../advantage_air/translations/ko.json | 20 + .../advantage_air/translations/lb.json | 20 + .../advantage_air/translations/nl.json | 20 + .../advantage_air/translations/no.json | 20 + .../advantage_air/translations/pl.json | 20 + .../advantage_air/translations/pt.json | 19 + .../advantage_air/translations/ru.json | 20 + .../advantage_air/translations/sl.json | 9 + .../advantage_air/translations/tr.json | 19 + .../advantage_air/translations/uk.json | 20 + .../advantage_air/translations/zh-Hans.json | 14 + .../advantage_air/translations/zh-Hant.json | 20 + .../components/aemet/__init__.py | 64 + .../components/aemet/config_flow.py | 85 + .../homeassistant/components/aemet/const.py | 326 +++ .../components/aemet/manifest.json | 9 + .../homeassistant/components/aemet/sensor.py | 168 ++ .../components/aemet/strings.json | 31 + .../components/aemet/translations/ca.json | 31 + .../components/aemet/translations/cs.json | 20 + .../components/aemet/translations/de.json | 22 + .../components/aemet/translations/en.json | 31 + .../components/aemet/translations/es-419.json | 12 + .../components/aemet/translations/es.json | 22 + .../components/aemet/translations/et.json | 31 + .../components/aemet/translations/fr.json | 22 + .../components/aemet/translations/hu.json | 21 + .../components/aemet/translations/id.json | 22 + .../components/aemet/translations/it.json | 31 + .../components/aemet/translations/ko.json | 22 + .../components/aemet/translations/lb.json | 15 + .../components/aemet/translations/nl.json | 31 + .../components/aemet/translations/no.json | 22 + .../components/aemet/translations/pl.json | 22 + .../components/aemet/translations/ru.json | 31 + .../aemet/translations/zh-Hant.json | 31 + .../homeassistant/components/aemet/weather.py | 113 + .../aemet/weather_update_coordinator.py | 643 ++++++ .../components/aftership/__init__.py | 1 + .../components/aftership/const.py | 42 + .../components/aftership/manifest.json | 8 + .../components/aftership/sensor.py | 211 ++ .../components/aftership/services.yaml | 43 + .../components/agent_dvr/__init__.py | 63 + .../agent_dvr/alarm_control_panel.py | 124 + .../components/agent_dvr/camera.py | 215 ++ .../components/agent_dvr/config_flow.py | 78 + .../components/agent_dvr/const.py | 11 + .../components/agent_dvr/helpers.py | 13 + .../components/agent_dvr/manifest.json | 9 + .../components/agent_dvr/services.yaml | 39 + .../components/agent_dvr/strings.json | 20 + .../components/agent_dvr/translations/ca.json | 20 + .../components/agent_dvr/translations/cs.json | 20 + .../components/agent_dvr/translations/de.json | 20 + .../components/agent_dvr/translations/en.json | 20 + .../agent_dvr/translations/es-419.json | 19 + .../components/agent_dvr/translations/es.json | 20 + .../components/agent_dvr/translations/et.json | 20 + .../components/agent_dvr/translations/fi.json | 19 + .../components/agent_dvr/translations/fr.json | 20 + .../components/agent_dvr/translations/he.json | 15 + .../components/agent_dvr/translations/hu.json | 19 + .../components/agent_dvr/translations/id.json | 20 + .../components/agent_dvr/translations/it.json | 20 + .../components/agent_dvr/translations/ka.json | 7 + .../components/agent_dvr/translations/ko.json | 20 + .../components/agent_dvr/translations/lb.json | 20 + .../components/agent_dvr/translations/nl.json | 20 + .../components/agent_dvr/translations/no.json | 20 + .../components/agent_dvr/translations/pl.json | 20 + .../agent_dvr/translations/pt-BR.json | 11 + .../components/agent_dvr/translations/pt.json | 19 + .../components/agent_dvr/translations/ru.json | 20 + .../components/agent_dvr/translations/sv.json | 19 + .../components/agent_dvr/translations/tr.json | 20 + .../components/agent_dvr/translations/uk.json | 20 + .../agent_dvr/translations/zh-Hans.json | 7 + .../agent_dvr/translations/zh-Hant.json | 20 + .../components/air_quality/__init__.py | 163 ++ .../components/air_quality/group.py | 13 + .../components/air_quality/manifest.json | 7 + .../components/airly/__init__.py | 194 ++ .../components/airly/air_quality.py | 143 ++ .../components/airly/config_flow.py | 119 + .../homeassistant/components/airly/const.py | 68 + .../components/airly/manifest.json | 10 + .../homeassistant/components/airly/model.py | 13 + .../homeassistant/components/airly/sensor.py | 114 + .../components/airly/strings.json | 30 + .../components/airly/system_health.py | 33 + .../components/airly/translations/bg.json | 19 + .../components/airly/translations/ca.json | 30 + .../components/airly/translations/cs.json | 28 + .../components/airly/translations/da.json | 22 + .../components/airly/translations/de.json | 30 + .../components/airly/translations/el.json | 7 + .../components/airly/translations/en.json | 30 + .../components/airly/translations/es-419.json | 28 + .../components/airly/translations/es.json | 30 + .../components/airly/translations/et.json | 30 + .../components/airly/translations/fr.json | 30 + .../components/airly/translations/he.json | 11 + .../components/airly/translations/hu.json | 29 + .../components/airly/translations/id.json | 30 + .../components/airly/translations/it.json | 30 + .../components/airly/translations/ko.json | 30 + .../components/airly/translations/lb.json | 28 + .../components/airly/translations/nl.json | 30 + .../components/airly/translations/nn.json | 9 + .../components/airly/translations/no.json | 30 + .../components/airly/translations/pl.json | 30 + .../components/airly/translations/pt.json | 21 + .../components/airly/translations/ru.json | 30 + .../components/airly/translations/sl.json | 27 + .../components/airly/translations/sv.json | 22 + .../components/airly/translations/tr.json | 24 + .../components/airly/translations/uk.json | 28 + .../airly/translations/zh-Hans.json | 20 + .../airly/translations/zh-Hant.json | 30 + .../components/airnow/__init__.py | 143 ++ .../components/airnow/config_flow.py | 109 + .../homeassistant/components/airnow/const.py | 21 + .../components/airnow/manifest.json | 9 + .../homeassistant/components/airnow/sensor.py | 119 + .../components/airnow/strings.json | 26 + .../components/airnow/translations/bg.json | 7 + .../components/airnow/translations/ca.json | 26 + .../components/airnow/translations/cs.json | 23 + .../components/airnow/translations/de.json | 25 + .../components/airnow/translations/en.json | 26 + .../airnow/translations/es-419.json | 17 + .../components/airnow/translations/es.json | 26 + .../components/airnow/translations/et.json | 26 + .../components/airnow/translations/fr.json | 26 + .../components/airnow/translations/hu.json | 24 + .../components/airnow/translations/id.json | 26 + .../components/airnow/translations/it.json | 26 + .../components/airnow/translations/ko.json | 26 + .../components/airnow/translations/lb.json | 24 + .../components/airnow/translations/nl.json | 26 + .../components/airnow/translations/no.json | 26 + .../components/airnow/translations/pl.json | 26 + .../components/airnow/translations/pt.json | 23 + .../components/airnow/translations/ru.json | 26 + .../components/airnow/translations/tr.json | 25 + .../components/airnow/translations/uk.json | 26 + .../airnow/translations/zh-Hant.json | 26 + .../components/airvisual/__init__.py | 374 +++ .../components/airvisual/air_quality.py | 110 + .../components/airvisual/config_flow.py | 266 +++ .../components/airvisual/const.py | 16 + .../components/airvisual/manifest.json | 9 + .../components/airvisual/sensor.py | 296 +++ .../components/airvisual/strings.json | 63 + .../components/airvisual/translations/ar.json | 11 + .../components/airvisual/translations/bg.json | 12 + .../components/airvisual/translations/ca.json | 63 + .../components/airvisual/translations/cs.json | 56 + .../components/airvisual/translations/de.json | 63 + .../components/airvisual/translations/el.json | 7 + .../components/airvisual/translations/en.json | 63 + .../airvisual/translations/es-419.json | 40 + .../components/airvisual/translations/es.json | 63 + .../components/airvisual/translations/et.json | 63 + .../components/airvisual/translations/fi.json | 14 + .../components/airvisual/translations/fr.json | 63 + .../components/airvisual/translations/he.json | 14 + .../components/airvisual/translations/hi.json | 17 + .../components/airvisual/translations/hu.json | 40 + .../components/airvisual/translations/id.json | 63 + .../components/airvisual/translations/it.json | 63 + .../components/airvisual/translations/ka.json | 15 + .../components/airvisual/translations/ko.json | 63 + .../components/airvisual/translations/lb.json | 51 + .../components/airvisual/translations/nl.json | 63 + .../components/airvisual/translations/no.json | 63 + .../components/airvisual/translations/pl.json | 63 + .../airvisual/translations/pt-BR.json | 15 + .../components/airvisual/translations/pt.json | 26 + .../components/airvisual/translations/ru.json | 63 + .../components/airvisual/translations/sl.json | 35 + .../components/airvisual/translations/sv.json | 19 + .../components/airvisual/translations/tr.json | 52 + .../components/airvisual/translations/uk.json | 43 + .../airvisual/translations/zh-Hans.json | 7 + .../airvisual/translations/zh-Hant.json | 63 + .../components/aladdin_connect/__init__.py | 1 + .../components/aladdin_connect/const.py | 19 + .../components/aladdin_connect/cover.py | 120 + .../components/aladdin_connect/manifest.json | 8 + .../components/aladdin_connect/model.py | 13 + .../alarm_control_panel/__init__.py | 196 ++ .../components/alarm_control_panel/const.py | 14 + .../alarm_control_panel/device_action.py | 145 ++ .../alarm_control_panel/device_condition.py | 163 ++ .../alarm_control_panel/device_trigger.py | 144 ++ .../components/alarm_control_panel/group.py | 30 + .../alarm_control_panel/manifest.json | 7 + .../alarm_control_panel/reproduce_state.py | 99 + .../alarm_control_panel/services.yaml | 85 + .../alarm_control_panel/strings.json | 40 + .../alarm_control_panel/translations/af.json | 17 + .../alarm_control_panel/translations/ar.json | 17 + .../alarm_control_panel/translations/bg.json | 33 + .../alarm_control_panel/translations/bs.json | 17 + .../alarm_control_panel/translations/ca.json | 40 + .../alarm_control_panel/translations/cs.json | 40 + .../alarm_control_panel/translations/cy.json | 17 + .../alarm_control_panel/translations/da.json | 33 + .../alarm_control_panel/translations/de.json | 40 + .../alarm_control_panel/translations/el.json | 17 + .../alarm_control_panel/translations/en.json | 40 + .../translations/es-419.json | 40 + .../alarm_control_panel/translations/es.json | 40 + .../alarm_control_panel/translations/et.json | 40 + .../alarm_control_panel/translations/eu.json | 9 + .../alarm_control_panel/translations/fa.json | 17 + .../alarm_control_panel/translations/fi.json | 17 + .../alarm_control_panel/translations/fr.json | 40 + .../alarm_control_panel/translations/gsw.json | 15 + .../alarm_control_panel/translations/he.json | 17 + .../alarm_control_panel/translations/hr.json | 17 + .../alarm_control_panel/translations/hu.json | 33 + .../alarm_control_panel/translations/hy.json | 17 + .../alarm_control_panel/translations/id.json | 40 + .../alarm_control_panel/translations/is.json | 16 + .../alarm_control_panel/translations/it.json | 40 + .../alarm_control_panel/translations/ja.json | 7 + .../alarm_control_panel/translations/ko.json | 40 + .../alarm_control_panel/translations/lb.json | 40 + .../alarm_control_panel/translations/lt.json | 13 + .../alarm_control_panel/translations/lv.json | 17 + .../alarm_control_panel/translations/nb.json | 17 + .../alarm_control_panel/translations/nl.json | 40 + .../alarm_control_panel/translations/nn.json | 17 + .../alarm_control_panel/translations/no.json | 40 + .../alarm_control_panel/translations/pl.json | 40 + .../translations/pt-BR.json | 40 + .../alarm_control_panel/translations/pt.json | 24 + .../alarm_control_panel/translations/ro.json | 17 + .../alarm_control_panel/translations/ru.json | 40 + .../alarm_control_panel/translations/sk.json | 17 + .../alarm_control_panel/translations/sl.json | 40 + .../alarm_control_panel/translations/sv.json | 33 + .../alarm_control_panel/translations/ta.json | 16 + .../alarm_control_panel/translations/te.json | 17 + .../alarm_control_panel/translations/th.json | 17 + .../alarm_control_panel/translations/tr.json | 23 + .../alarm_control_panel/translations/uk.json | 40 + .../alarm_control_panel/translations/vi.json | 17 + .../translations/zh-Hans.json | 40 + .../translations/zh-Hant.json | 40 + .../components/alarmdecoder/__init__.py | 156 ++ .../alarmdecoder/alarm_control_panel.py | 219 ++ .../components/alarmdecoder/binary_sensor.py | 177 ++ .../components/alarmdecoder/config_flow.py | 365 +++ .../components/alarmdecoder/const.py | 49 + .../components/alarmdecoder/manifest.json | 9 + .../components/alarmdecoder/sensor.py | 60 + .../components/alarmdecoder/services.yaml | 31 + .../components/alarmdecoder/strings.json | 72 + .../alarmdecoder/translations/ca.json | 74 + .../alarmdecoder/translations/cs.json | 69 + .../alarmdecoder/translations/de.json | 56 + .../alarmdecoder/translations/el.json | 71 + .../alarmdecoder/translations/en.json | 74 + .../alarmdecoder/translations/es-419.json | 36 + .../alarmdecoder/translations/es.json | 74 + .../alarmdecoder/translations/et.json | 74 + .../alarmdecoder/translations/fr.json | 74 + .../alarmdecoder/translations/hu.json | 40 + .../alarmdecoder/translations/id.json | 74 + .../alarmdecoder/translations/it.json | 74 + .../alarmdecoder/translations/ko.json | 74 + .../alarmdecoder/translations/lb.json | 74 + .../alarmdecoder/translations/nl.json | 74 + .../alarmdecoder/translations/no.json | 74 + .../alarmdecoder/translations/pl.json | 74 + .../alarmdecoder/translations/pt.json | 33 + .../alarmdecoder/translations/ru.json | 74 + .../alarmdecoder/translations/sl.json | 7 + .../alarmdecoder/translations/sv.json | 27 + .../alarmdecoder/translations/tr.json | 48 + .../alarmdecoder/translations/uk.json | 74 + .../alarmdecoder/translations/zh-Hans.json | 14 + .../alarmdecoder/translations/zh-Hant.json | 74 + .../components/alert/__init__.py | 331 +++ .../components/alert/manifest.json | 9 + .../components/alert/reproduce_state.py | 78 + .../components/alert/services.yaml | 20 + .../components/alexa/__init__.py | 103 + .../homeassistant/components/alexa/auth.py | 162 ++ .../components/alexa/capabilities.py | 2023 +++++++++++++++++ .../homeassistant/components/alexa/config.py | 89 + .../homeassistant/components/alexa/const.py | 205 ++ .../components/alexa/entities.py | 885 +++++++ .../homeassistant/components/alexa/errors.py | 120 + .../components/alexa/flash_briefings.py | 121 + .../components/alexa/handlers.py | 1524 +++++++++++++ .../homeassistant/components/alexa/intent.py | 294 +++ .../homeassistant/components/alexa/logbook.py | 28 + .../components/alexa/manifest.json | 9 + .../components/alexa/messages.py | 195 ++ .../components/alexa/resources.py | 399 ++++ .../components/alexa/services.yaml | 0 .../components/alexa/smart_home.py | 66 + .../components/alexa/smart_home_http.py | 122 + .../components/alexa/state_report.py | 291 +++ .../components/almond/__init__.py | 311 +++ .../components/almond/config_flow.py | 120 + .../homeassistant/components/almond/const.py | 4 + .../components/almond/manifest.json | 10 + .../components/almond/strings.json | 19 + .../components/almond/translations/bg.json | 13 + .../components/almond/translations/ca.json | 19 + .../components/almond/translations/cs.json | 19 + .../components/almond/translations/da.json | 17 + .../components/almond/translations/de.json | 19 + .../components/almond/translations/en.json | 19 + .../almond/translations/es-419.json | 17 + .../components/almond/translations/es.json | 19 + .../components/almond/translations/et.json | 19 + .../components/almond/translations/fi.json | 7 + .../components/almond/translations/fr.json | 19 + .../components/almond/translations/hu.json | 19 + .../components/almond/translations/id.json | 19 + .../components/almond/translations/it.json | 19 + .../components/almond/translations/ko.json | 19 + .../components/almond/translations/lb.json | 19 + .../components/almond/translations/nl.json | 19 + .../components/almond/translations/no.json | 19 + .../components/almond/translations/pl.json | 19 + .../components/almond/translations/pt-BR.json | 9 + .../components/almond/translations/pt.json | 15 + .../components/almond/translations/ru.json | 19 + .../components/almond/translations/sl.json | 17 + .../components/almond/translations/sv.json | 17 + .../components/almond/translations/tr.json | 8 + .../components/almond/translations/uk.json | 19 + .../almond/translations/zh-Hant.json | 19 + .../components/alpha_vantage/__init__.py | 1 + .../components/alpha_vantage/manifest.json | 8 + .../components/alpha_vantage/sensor.py | 218 ++ .../components/amazon_polly/__init__.py | 1 + .../components/amazon_polly/const.py | 131 ++ .../components/amazon_polly/manifest.json | 8 + .../components/amazon_polly/tts.py | 196 ++ .../components/ambiclimate/__init__.py | 43 + .../components/ambiclimate/climate.py | 244 ++ .../components/ambiclimate/config_flow.py | 153 ++ .../components/ambiclimate/const.py | 15 + .../components/ambiclimate/manifest.json | 10 + .../components/ambiclimate/services.yaml | 54 + .../components/ambiclimate/strings.json | 22 + .../ambiclimate/translations/bg.json | 20 + .../ambiclimate/translations/ca.json | 22 + .../ambiclimate/translations/cs.json | 22 + .../ambiclimate/translations/da.json | 20 + .../ambiclimate/translations/de.json | 22 + .../ambiclimate/translations/en.json | 22 + .../ambiclimate/translations/es-419.json | 20 + .../ambiclimate/translations/es.json | 22 + .../ambiclimate/translations/et.json | 22 + .../ambiclimate/translations/fr.json | 22 + .../ambiclimate/translations/hu.json | 11 + .../ambiclimate/translations/id.json | 22 + .../ambiclimate/translations/it.json | 22 + .../ambiclimate/translations/ka.json | 8 + .../ambiclimate/translations/ko.json | 22 + .../ambiclimate/translations/lb.json | 22 + .../ambiclimate/translations/nl.json | 22 + .../ambiclimate/translations/no.json | 22 + .../ambiclimate/translations/pl.json | 22 + .../ambiclimate/translations/pt-BR.json | 20 + .../ambiclimate/translations/pt.json | 11 + .../ambiclimate/translations/ru.json | 22 + .../ambiclimate/translations/sl.json | 20 + .../ambiclimate/translations/sv.json | 20 + .../ambiclimate/translations/tr.json | 7 + .../ambiclimate/translations/uk.json | 22 + .../ambiclimate/translations/zh-Hans.json | 8 + .../ambiclimate/translations/zh-Hant.json | 22 + .../components/ambient_station/__init__.py | 573 +++++ .../ambient_station/binary_sensor.py | 84 + .../components/ambient_station/config_flow.py | 62 + .../components/ambient_station/const.py | 12 + .../components/ambient_station/manifest.json | 9 + .../components/ambient_station/sensor.py | 87 + .../components/ambient_station/strings.json | 20 + .../ambient_station/translations/bg.json | 17 + .../ambient_station/translations/ca.json | 20 + .../ambient_station/translations/cs.json | 20 + .../ambient_station/translations/da.json | 20 + .../ambient_station/translations/de.json | 20 + .../ambient_station/translations/en.json | 20 + .../ambient_station/translations/es-419.json | 20 + .../ambient_station/translations/es.json | 20 + .../ambient_station/translations/et.json | 20 + .../ambient_station/translations/fi.json | 12 + .../ambient_station/translations/fr.json | 20 + .../ambient_station/translations/he.json | 15 + .../ambient_station/translations/hu.json | 20 + .../ambient_station/translations/id.json | 20 + .../ambient_station/translations/it.json | 20 + .../ambient_station/translations/ko.json | 20 + .../ambient_station/translations/lb.json | 20 + .../ambient_station/translations/nl.json | 20 + .../ambient_station/translations/no.json | 20 + .../ambient_station/translations/pl.json | 20 + .../ambient_station/translations/pt-BR.json | 17 + .../ambient_station/translations/pt.json | 20 + .../ambient_station/translations/ru.json | 20 + .../ambient_station/translations/sl.json | 20 + .../ambient_station/translations/sv.json | 17 + .../ambient_station/translations/th.json | 16 + .../ambient_station/translations/tr.json | 17 + .../ambient_station/translations/uk.json | 20 + .../ambient_station/translations/zh-Hans.json | 17 + .../ambient_station/translations/zh-Hant.json | 20 + .../components/amcrest/__init__.py | 359 +++ .../components/amcrest/binary_sensor.py | 211 ++ .../components/amcrest/camera.py | 597 +++++ .../homeassistant/components/amcrest/const.py | 19 + .../components/amcrest/helpers.py | 21 + .../components/amcrest/manifest.json | 9 + .../components/amcrest/sensor.py | 135 ++ .../components/amcrest/services.yaml | 165 ++ .../components/ampio/__init__.py | 1 + .../components/ampio/air_quality.py | 105 + .../homeassistant/components/ampio/const.py | 7 + .../components/ampio/manifest.json | 8 + .../components/analytics/__init__.py | 76 + .../components/analytics/analytics.py | 261 +++ .../components/analytics/const.py | 51 + .../components/analytics/manifest.json | 9 + .../components/android_ip_webcam/__init__.py | 333 +++ .../android_ip_webcam/binary_sensor.py | 53 + .../android_ip_webcam/manifest.json | 8 + .../components/android_ip_webcam/sensor.py | 77 + .../components/android_ip_webcam/switch.py | 88 + .../components/androidtv/__init__.py | 1 + .../components/androidtv/manifest.json | 12 + .../components/androidtv/media_player.py | 788 +++++++ .../components/androidtv/services.yaml | 80 + .../components/anel_pwrctrl/__init__.py | 1 + .../components/anel_pwrctrl/manifest.json | 8 + .../components/anel_pwrctrl/switch.py | 107 + .../components/anthemav/__init__.py | 1 + .../components/anthemav/manifest.json | 8 + .../components/anthemav/media_player.py | 189 ++ .../components/apache_kafka/__init__.py | 142 ++ .../components/apache_kafka/manifest.json | 8 + .../components/apcupsd/__init__.py | 85 + .../components/apcupsd/binary_sensor.py | 44 + .../components/apcupsd/manifest.json | 8 + .../components/apcupsd/sensor.py | 200 ++ .../homeassistant/components/api/__init__.py | 448 ++++ .../components/api/manifest.json | 8 + .../components/api/services.yaml | 0 .../homeassistant/components/apns/__init__.py | 1 + .../homeassistant/components/apns/const.py | 2 + .../components/apns/manifest.json | 9 + .../homeassistant/components/apns/notify.py | 263 +++ .../components/apns/services.yaml | 0 .../components/apple_tv/__init__.py | 385 ++++ .../components/apple_tv/config_flow.py | 402 ++++ .../components/apple_tv/const.py | 11 + .../components/apple_tv/manifest.json | 11 + .../components/apple_tv/media_player.py | 312 +++ .../components/apple_tv/remote.py | 67 + .../components/apple_tv/strings.json | 64 + .../components/apple_tv/translations/ca.json | 64 + .../components/apple_tv/translations/cs.json | 60 + .../components/apple_tv/translations/de.json | 64 + .../components/apple_tv/translations/en.json | 64 + .../components/apple_tv/translations/es.json | 64 + .../components/apple_tv/translations/et.json | 64 + .../components/apple_tv/translations/fr.json | 64 + .../components/apple_tv/translations/hu.json | 44 + .../components/apple_tv/translations/id.json | 64 + .../components/apple_tv/translations/it.json | 64 + .../components/apple_tv/translations/ko.json | 64 + .../components/apple_tv/translations/lb.json | 60 + .../components/apple_tv/translations/nl.json | 64 + .../components/apple_tv/translations/no.json | 64 + .../components/apple_tv/translations/pl.json | 64 + .../components/apple_tv/translations/pt.json | 61 + .../components/apple_tv/translations/ru.json | 64 + .../components/apple_tv/translations/sl.json | 64 + .../components/apple_tv/translations/tr.json | 56 + .../components/apple_tv/translations/uk.json | 64 + .../apple_tv/translations/zh-Hans.json | 32 + .../apple_tv/translations/zh-Hant.json | 64 + .../components/apprise/__init__.py | 1 + .../components/apprise/manifest.json | 8 + .../components/apprise/notify.py | 69 + .../homeassistant/components/aprs/__init__.py | 1 + .../components/aprs/device_tracker.py | 183 ++ .../components/aprs/manifest.json | 8 + .../components/aqualogic/__init__.py | 90 + .../components/aqualogic/manifest.json | 8 + .../components/aqualogic/sensor.py | 111 + .../components/aqualogic/switch.py | 103 + .../components/aquostv/__init__.py | 1 + .../components/aquostv/manifest.json | 8 + .../components/aquostv/media_player.py | 263 +++ .../components/arcam_fmj/__init__.py | 113 + .../components/arcam_fmj/config_flow.py | 98 + .../components/arcam_fmj/const.py | 15 + .../components/arcam_fmj/device_trigger.py | 82 + .../components/arcam_fmj/manifest.json | 15 + .../components/arcam_fmj/media_player.py | 405 ++++ .../components/arcam_fmj/strings.json | 28 + .../components/arcam_fmj/translations/ca.json | 27 + .../components/arcam_fmj/translations/cs.json | 27 + .../components/arcam_fmj/translations/de.json | 27 + .../components/arcam_fmj/translations/en.json | 27 + .../components/arcam_fmj/translations/es.json | 27 + .../components/arcam_fmj/translations/et.json | 27 + .../components/arcam_fmj/translations/fr.json | 31 + .../components/arcam_fmj/translations/hu.json | 17 + .../components/arcam_fmj/translations/id.json | 27 + .../components/arcam_fmj/translations/it.json | 27 + .../components/arcam_fmj/translations/ko.json | 27 + .../components/arcam_fmj/translations/lb.json | 27 + .../components/arcam_fmj/translations/nl.json | 31 + .../components/arcam_fmj/translations/no.json | 27 + .../components/arcam_fmj/translations/pl.json | 33 + .../arcam_fmj/translations/pt-BR.json | 7 + .../components/arcam_fmj/translations/pt.json | 22 + .../components/arcam_fmj/translations/ro.json | 9 + .../components/arcam_fmj/translations/ru.json | 27 + .../components/arcam_fmj/translations/tr.json | 17 + .../components/arcam_fmj/translations/uk.json | 27 + .../arcam_fmj/translations/zh-Hans.json | 7 + .../arcam_fmj/translations/zh-Hant.json | 27 + .../components/arduino/__init__.py | 115 + .../components/arduino/manifest.json | 8 + .../components/arduino/sensor.py | 58 + .../components/arduino/switch.py | 80 + .../components/arest/__init__.py | 1 + .../components/arest/binary_sensor.py | 122 + .../components/arest/manifest.json | 7 + .../homeassistant/components/arest/sensor.py | 218 ++ .../homeassistant/components/arest/switch.py | 210 ++ .../homeassistant/components/arlo/__init__.py | 87 + .../components/arlo/alarm_control_panel.py | 158 ++ .../homeassistant/components/arlo/camera.py | 167 ++ .../components/arlo/manifest.json | 9 + .../homeassistant/components/arlo/sensor.py | 195 ++ .../components/arlo/services.yaml | 5 + .../components/arris_tg2492lg/__init__.py | 1 + .../arris_tg2492lg/device_tracker.py | 67 + .../components/arris_tg2492lg/manifest.json | 8 + .../components/aruba/__init__.py | 1 + .../components/aruba/device_tracker.py | 135 ++ .../components/aruba/manifest.json | 8 + .../homeassistant/components/arwn/__init__.py | 1 + .../components/arwn/manifest.json | 8 + .../homeassistant/components/arwn/sensor.py | 174 ++ .../components/asterisk_cdr/__init__.py | 1 + .../components/asterisk_cdr/mailbox.py | 61 + .../components/asterisk_cdr/manifest.json | 8 + .../components/asterisk_mbox/__init__.py | 123 + .../components/asterisk_mbox/mailbox.py | 74 + .../components/asterisk_mbox/manifest.json | 8 + .../components/asuswrt/__init__.py | 164 ++ .../components/asuswrt/config_flow.py | 236 ++ .../homeassistant/components/asuswrt/const.py | 28 + .../components/asuswrt/device_tracker.py | 138 ++ .../components/asuswrt/manifest.json | 9 + .../components/asuswrt/router.py | 426 ++++ .../components/asuswrt/sensor.py | 173 ++ .../components/asuswrt/strings.json | 45 + .../components/asuswrt/translations/bg.json | 17 + .../components/asuswrt/translations/ca.json | 45 + .../components/asuswrt/translations/cs.json | 25 + .../components/asuswrt/translations/de.json | 41 + .../components/asuswrt/translations/en.json | 45 + .../components/asuswrt/translations/es.json | 45 + .../components/asuswrt/translations/et.json | 45 + .../components/asuswrt/translations/fr.json | 45 + .../components/asuswrt/translations/hu.json | 33 + .../components/asuswrt/translations/id.json | 45 + .../components/asuswrt/translations/it.json | 45 + .../components/asuswrt/translations/ko.json | 45 + .../components/asuswrt/translations/lb.json | 12 + .../components/asuswrt/translations/nl.json | 45 + .../components/asuswrt/translations/no.json | 45 + .../components/asuswrt/translations/pl.json | 45 + .../components/asuswrt/translations/ru.json | 45 + .../asuswrt/translations/zh-Hant.json | 45 + .../homeassistant/components/atag/__init__.py | 101 + .../homeassistant/components/atag/climate.py | 102 + .../components/atag/config_flow.py | 48 + .../components/atag/manifest.json | 9 + .../homeassistant/components/atag/sensor.py | 72 + .../components/atag/strings.json | 20 + .../components/atag/translations/ca.json | 20 + .../components/atag/translations/cs.json | 20 + .../components/atag/translations/de.json | 20 + .../components/atag/translations/en.json | 20 + .../components/atag/translations/es-419.json | 16 + .../components/atag/translations/es.json | 20 + .../components/atag/translations/et.json | 20 + .../components/atag/translations/fi.json | 13 + .../components/atag/translations/fr.json | 20 + .../components/atag/translations/hi.json | 13 + .../components/atag/translations/hu.json | 18 + .../components/atag/translations/id.json | 20 + .../components/atag/translations/it.json | 20 + .../components/atag/translations/ka.json | 7 + .../components/atag/translations/ko.json | 20 + .../components/atag/translations/lb.json | 20 + .../components/atag/translations/nl.json | 20 + .../components/atag/translations/no.json | 20 + .../components/atag/translations/pl.json | 20 + .../components/atag/translations/pt-BR.json | 7 + .../components/atag/translations/pt.json | 18 + .../components/atag/translations/ru.json | 20 + .../components/atag/translations/sl.json | 16 + .../components/atag/translations/sv.json | 13 + .../components/atag/translations/tr.json | 20 + .../components/atag/translations/uk.json | 20 + .../components/atag/translations/zh-Hans.json | 7 + .../components/atag/translations/zh-Hant.json | 20 + .../components/atag/water_heater.py | 69 + .../components/aten_pe/__init__.py | 1 + .../components/aten_pe/manifest.json | 8 + .../components/aten_pe/switch.py | 122 + .../components/atome/__init__.py | 1 + .../components/atome/manifest.json | 8 + .../homeassistant/components/atome/sensor.py | 277 +++ .../components/august/__init__.py | 330 +++ .../components/august/activity.py | 185 ++ .../components/august/binary_sensor.py | 284 +++ .../homeassistant/components/august/camera.py | 88 + .../components/august/config_flow.py | 179 ++ .../homeassistant/components/august/const.py | 46 + .../homeassistant/components/august/entity.py | 78 + .../components/august/exceptions.py | 15 + .../components/august/gateway.py | 142 ++ .../homeassistant/components/august/lock.py | 136 ++ .../components/august/manifest.json | 23 + .../homeassistant/components/august/sensor.py | 274 +++ .../components/august/strings.json | 38 + .../components/august/subscriber.py | 76 + .../components/august/translations/ca.json | 38 + .../components/august/translations/cs.json | 33 + .../components/august/translations/da.json | 21 + .../components/august/translations/de.json | 38 + .../components/august/translations/el.json | 22 + .../components/august/translations/en.json | 38 + .../august/translations/es-419.json | 21 + .../components/august/translations/es.json | 38 + .../components/august/translations/et.json | 38 + .../components/august/translations/fr.json | 38 + .../components/august/translations/hu.json | 36 + .../components/august/translations/id.json | 38 + .../components/august/translations/it.json | 38 + .../components/august/translations/ko.json | 38 + .../components/august/translations/lb.json | 30 + .../components/august/translations/nl.json | 38 + .../components/august/translations/no.json | 38 + .../components/august/translations/pl.json | 38 + .../components/august/translations/pt-BR.json | 13 + .../components/august/translations/pt.json | 20 + .../components/august/translations/ru.json | 38 + .../components/august/translations/sl.json | 21 + .../components/august/translations/sv.json | 31 + .../components/august/translations/tr.json | 22 + .../components/august/translations/uk.json | 22 + .../august/translations/zh-Hant.json | 38 + .../components/aurora/__init__.py | 169 ++ .../components/aurora/binary_sensor.py | 24 + .../components/aurora/config_flow.py | 110 + .../homeassistant/components/aurora/const.py | 14 + .../components/aurora/manifest.json | 9 + .../homeassistant/components/aurora/sensor.py | 33 + .../components/aurora/strings.json | 26 + .../components/aurora/translations/ca.json | 26 + .../components/aurora/translations/cs.json | 26 + .../components/aurora/translations/de.json | 26 + .../components/aurora/translations/en.json | 26 + .../components/aurora/translations/es.json | 26 + .../components/aurora/translations/et.json | 26 + .../components/aurora/translations/fr.json | 26 + .../components/aurora/translations/hu.json | 26 + .../components/aurora/translations/id.json | 26 + .../components/aurora/translations/it.json | 26 + .../components/aurora/translations/ka.json | 26 + .../components/aurora/translations/ko.json | 26 + .../components/aurora/translations/lb.json | 26 + .../components/aurora/translations/nl.json | 26 + .../components/aurora/translations/no.json | 26 + .../components/aurora/translations/pl.json | 26 + .../components/aurora/translations/pt.json | 16 + .../components/aurora/translations/ru.json | 26 + .../components/aurora/translations/sl.json | 11 + .../components/aurora/translations/tr.json | 16 + .../components/aurora/translations/uk.json | 26 + .../aurora/translations/zh-Hans.json | 11 + .../aurora/translations/zh-Hant.json | 26 + .../aurora_abb_powerone/__init__.py | 1 + .../aurora_abb_powerone/manifest.json | 8 + .../components/aurora_abb_powerone/sensor.py | 102 + .../homeassistant/components/auth/__init__.py | 581 +++++ .../components/auth/indieauth.py | 201 ++ .../components/auth/login_flow.py | 270 +++ .../components/auth/manifest.json | 8 + .../components/auth/mfa_setup_flow.py | 148 ++ .../components/auth/strings.json | 35 + .../components/auth/translations/ar.json | 7 + .../components/auth/translations/bg.json | 35 + .../components/auth/translations/ca.json | 35 + .../components/auth/translations/cs.json | 34 + .../components/auth/translations/da.json | 35 + .../components/auth/translations/de.json | 35 + .../components/auth/translations/en.json | 35 + .../components/auth/translations/es-419.json | 35 + .../components/auth/translations/es.json | 35 + .../components/auth/translations/et.json | 35 + .../components/auth/translations/fi.json | 18 + .../components/auth/translations/fr.json | 35 + .../components/auth/translations/he.json | 35 + .../components/auth/translations/hu.json | 35 + .../components/auth/translations/id.json | 35 + .../components/auth/translations/it.json | 35 + .../components/auth/translations/ko.json | 35 + .../components/auth/translations/lb.json | 35 + .../components/auth/translations/nl.json | 35 + .../components/auth/translations/nn.json | 16 + .../components/auth/translations/no.json | 35 + .../components/auth/translations/pl.json | 35 + .../components/auth/translations/pt-BR.json | 35 + .../components/auth/translations/pt.json | 35 + .../components/auth/translations/ro.json | 34 + .../components/auth/translations/ru.json | 35 + .../components/auth/translations/sl.json | 35 + .../components/auth/translations/sv.json | 35 + .../components/auth/translations/th.json | 11 + .../components/auth/translations/tr.json | 22 + .../components/auth/translations/uk.json | 35 + .../components/auth/translations/vi.json | 16 + .../components/auth/translations/zh-Hans.json | 35 + .../components/auth/translations/zh-Hant.json | 35 + .../components/automation/__init__.py | 766 +++++++ .../automation/blueprints/motion_light.yaml | 54 + .../blueprints/notify_leaving_zone.yaml | 44 + .../components/automation/config.py | 141 ++ .../components/automation/const.py | 19 + .../components/automation/helpers.py | 15 + .../components/automation/logbook.py | 31 + .../components/automation/manifest.json | 9 + .../components/automation/reproduce_state.py | 76 + .../components/automation/services.yaml | 48 + .../components/automation/strings.json | 9 + .../components/automation/trace.py | 57 + .../automation/translations/af.json | 9 + .../automation/translations/ar.json | 9 + .../automation/translations/bg.json | 9 + .../automation/translations/bs.json | 9 + .../automation/translations/ca.json | 9 + .../automation/translations/cs.json | 9 + .../automation/translations/cy.json | 9 + .../automation/translations/da.json | 9 + .../automation/translations/de.json | 9 + .../automation/translations/el.json | 9 + .../automation/translations/en.json | 9 + .../automation/translations/es-419.json | 9 + .../automation/translations/es.json | 9 + .../automation/translations/et.json | 9 + .../automation/translations/eu.json | 9 + .../automation/translations/fa.json | 9 + .../automation/translations/fi.json | 9 + .../automation/translations/fr.json | 9 + .../automation/translations/gsw.json | 9 + .../automation/translations/he.json | 9 + .../automation/translations/hi.json | 8 + .../automation/translations/hr.json | 9 + .../automation/translations/hu.json | 9 + .../automation/translations/hy.json | 9 + .../automation/translations/id.json | 9 + .../automation/translations/is.json | 9 + .../automation/translations/it.json | 9 + .../automation/translations/ja.json | 9 + .../automation/translations/ko.json | 9 + .../automation/translations/lb.json | 9 + .../automation/translations/lt.json | 8 + .../automation/translations/lv.json | 9 + .../automation/translations/nb.json | 9 + .../automation/translations/nl.json | 9 + .../automation/translations/nn.json | 9 + .../automation/translations/no.json | 9 + .../automation/translations/pl.json | 9 + .../automation/translations/pt-BR.json | 9 + .../automation/translations/pt.json | 9 + .../automation/translations/ro.json | 9 + .../automation/translations/ru.json | 9 + .../automation/translations/sk.json | 9 + .../automation/translations/sl.json | 9 + .../automation/translations/sv.json | 9 + .../automation/translations/ta.json | 8 + .../automation/translations/te.json | 9 + .../automation/translations/th.json | 9 + .../automation/translations/tr.json | 9 + .../automation/translations/uk.json | 9 + .../automation/translations/vi.json | 9 + .../automation/translations/zh-Hans.json | 9 + .../automation/translations/zh-Hant.json | 9 + .../homeassistant/components/avea/__init__.py | 1 + .../homeassistant/components/avea/light.py | 87 + .../components/avea/manifest.json | 8 + .../components/avion/__init__.py | 1 + .../homeassistant/components/avion/light.py | 141 ++ .../components/avion/manifest.json | 8 + .../components/awair/__init__.py | 80 + .../components/awair/config_flow.py | 103 + .../homeassistant/components/awair/const.py | 122 + .../components/awair/manifest.json | 9 + .../homeassistant/components/awair/sensor.py | 238 ++ .../components/awair/strings.json | 29 + .../components/awair/translations/ca.json | 29 + .../components/awair/translations/cs.json | 28 + .../components/awair/translations/de.json | 29 + .../components/awair/translations/en.json | 29 + .../components/awair/translations/es.json | 29 + .../components/awair/translations/et.json | 29 + .../components/awair/translations/fr.json | 29 + .../components/awair/translations/hu.json | 28 + .../components/awair/translations/id.json | 29 + .../components/awair/translations/it.json | 29 + .../components/awair/translations/ko.json | 29 + .../components/awair/translations/lb.json | 29 + .../components/awair/translations/nl.json | 29 + .../components/awair/translations/no.json | 29 + .../components/awair/translations/pl.json | 29 + .../components/awair/translations/pt-BR.json | 7 + .../components/awair/translations/pt.json | 27 + .../components/awair/translations/ru.json | 29 + .../components/awair/translations/tr.json | 27 + .../components/awair/translations/uk.json | 29 + .../awair/translations/zh-Hant.json | 29 + .../homeassistant/components/aws/__init__.py | 178 ++ .../components/aws/config_flow.py | 18 + .../homeassistant/components/aws/const.py | 15 + .../components/aws/manifest.json | 8 + .../homeassistant/components/aws/notify.py | 231 ++ .../homeassistant/components/axis/__init__.py | 78 + .../components/axis/axis_base.py | 76 + .../components/axis/binary_sensor.py | 134 ++ .../homeassistant/components/axis/camera.py | 116 + .../components/axis/config_flow.py | 275 +++ .../homeassistant/components/axis/const.py | 25 + .../homeassistant/components/axis/device.py | 296 +++ .../homeassistant/components/axis/errors.py | 22 + .../homeassistant/components/axis/light.py | 119 + .../components/axis/manifest.json | 44 + .../components/axis/strings.json | 37 + .../homeassistant/components/axis/switch.py | 54 + .../components/axis/translations/bg.json | 25 + .../components/axis/translations/ca.json | 37 + .../components/axis/translations/cs.json | 37 + .../components/axis/translations/da.json | 25 + .../components/axis/translations/de.json | 37 + .../components/axis/translations/en.json | 37 + .../components/axis/translations/es-419.json | 25 + .../components/axis/translations/es.json | 37 + .../components/axis/translations/et.json | 37 + .../components/axis/translations/fi.json | 9 + .../components/axis/translations/fr.json | 37 + .../components/axis/translations/he.json | 11 + .../components/axis/translations/hu.json | 33 + .../components/axis/translations/id.json | 37 + .../components/axis/translations/it.json | 37 + .../components/axis/translations/ko.json | 37 + .../components/axis/translations/lb.json | 37 + .../components/axis/translations/nl.json | 37 + .../components/axis/translations/nn.json | 14 + .../components/axis/translations/no.json | 37 + .../components/axis/translations/pl.json | 37 + .../components/axis/translations/pt-BR.json | 25 + .../components/axis/translations/pt.json | 24 + .../components/axis/translations/ru.json | 37 + .../components/axis/translations/sl.json | 25 + .../components/axis/translations/sv.json | 25 + .../components/axis/translations/th.json | 13 + .../components/axis/translations/tr.json | 23 + .../components/axis/translations/uk.json | 37 + .../components/axis/translations/zh-Hans.json | 18 + .../components/axis/translations/zh-Hant.json | 37 + .../components/azure_devops/__init__.py | 118 + .../components/azure_devops/config_flow.py | 126 + .../components/azure_devops/const.py | 11 + .../components/azure_devops/manifest.json | 9 + .../components/azure_devops/sensor.py | 150 ++ .../components/azure_devops/strings.json | 32 + .../azure_devops/translations/ca.json | 32 + .../azure_devops/translations/cs.json | 31 + .../azure_devops/translations/de.json | 32 + .../azure_devops/translations/en.json | 32 + .../azure_devops/translations/es.json | 32 + .../azure_devops/translations/et.json | 32 + .../azure_devops/translations/fr.json | 32 + .../azure_devops/translations/hu.json | 20 + .../azure_devops/translations/id.json | 32 + .../azure_devops/translations/it.json | 32 + .../azure_devops/translations/ka.json | 8 + .../azure_devops/translations/ko.json | 32 + .../azure_devops/translations/lb.json | 32 + .../azure_devops/translations/nl.json | 32 + .../azure_devops/translations/no.json | 32 + .../azure_devops/translations/pl.json | 32 + .../azure_devops/translations/pt-BR.json | 23 + .../azure_devops/translations/pt.json | 12 + .../azure_devops/translations/ru.json | 32 + .../azure_devops/translations/tr.json | 28 + .../azure_devops/translations/uk.json | 32 + .../azure_devops/translations/zh-Hans.json | 8 + .../azure_devops/translations/zh-Hant.json | 32 + .../components/azure_event_hub/__init__.py | 225 ++ .../components/azure_event_hub/const.py | 13 + .../components/azure_event_hub/manifest.json | 8 + .../components/azure_service_bus/__init__.py | 1 + .../azure_service_bus/manifest.json | 8 + .../components/azure_service_bus/notify.py | 106 + .../components/baidu/__init__.py | 1 + .../components/baidu/manifest.json | 8 + .../homeassistant/components/baidu/tts.py | 134 ++ .../components/bayesian/__init__.py | 4 + .../components/bayesian/binary_sensor.py | 418 ++++ .../components/bayesian/manifest.json | 8 + .../components/bayesian/services.yaml | 3 + .../components/bbb_gpio/__init__.py | 51 + .../components/bbb_gpio/binary_sensor.py | 77 + .../components/bbb_gpio/manifest.json | 8 + .../components/bbb_gpio/switch.py | 79 + .../homeassistant/components/bbox/__init__.py | 1 + .../components/bbox/device_tracker.py | 97 + .../components/bbox/manifest.json | 8 + .../homeassistant/components/bbox/sensor.py | 208 ++ .../components/beewi_smartclim/__init__.py | 1 + .../components/beewi_smartclim/manifest.json | 8 + .../components/beewi_smartclim/sensor.py | 104 + .../components/bh1750/__init__.py | 1 + .../components/bh1750/manifest.json | 8 + .../homeassistant/components/bh1750/sensor.py | 138 ++ .../components/binary_sensor/__init__.py | 175 ++ .../binary_sensor/device_condition.py | 271 +++ .../binary_sensor/device_trigger.py | 252 ++ .../components/binary_sensor/group.py | 14 + .../components/binary_sensor/manifest.json | 7 + .../binary_sensor/significant_change.py | 22 + .../components/binary_sensor/strings.json | 191 ++ .../binary_sensor/translations/af.json | 85 + .../binary_sensor/translations/ar.json | 85 + .../binary_sensor/translations/bg.json | 175 ++ .../binary_sensor/translations/bs.json | 61 + .../binary_sensor/translations/ca.json | 191 ++ .../binary_sensor/translations/cs.json | 191 ++ .../binary_sensor/translations/cy.json | 85 + .../binary_sensor/translations/da.json | 175 ++ .../binary_sensor/translations/de.json | 191 ++ .../binary_sensor/translations/el.json | 85 + .../binary_sensor/translations/en.json | 191 ++ .../binary_sensor/translations/es-419.json | 175 ++ .../binary_sensor/translations/es.json | 191 ++ .../binary_sensor/translations/et.json | 191 ++ .../binary_sensor/translations/eu.json | 60 + .../binary_sensor/translations/fa.json | 85 + .../binary_sensor/translations/fi.json | 85 + .../binary_sensor/translations/fr.json | 191 ++ .../binary_sensor/translations/gsw.json | 64 + .../binary_sensor/translations/he.json | 95 + .../binary_sensor/translations/hi.json | 45 + .../binary_sensor/translations/hr.json | 85 + .../binary_sensor/translations/hu.json | 191 ++ .../binary_sensor/translations/hy.json | 85 + .../binary_sensor/translations/id.json | 191 ++ .../binary_sensor/translations/is.json | 80 + .../binary_sensor/translations/it.json | 191 ++ .../binary_sensor/translations/ja.json | 84 + .../binary_sensor/translations/ka.json | 20 + .../binary_sensor/translations/ko.json | 191 ++ .../binary_sensor/translations/lb.json | 187 ++ .../binary_sensor/translations/lt.json | 60 + .../binary_sensor/translations/lv.json | 91 + .../binary_sensor/translations/nb.json | 85 + .../binary_sensor/translations/nl.json | 191 ++ .../binary_sensor/translations/nn.json | 85 + .../binary_sensor/translations/no.json | 191 ++ .../binary_sensor/translations/pl.json | 191 ++ .../binary_sensor/translations/pt-BR.json | 85 + .../binary_sensor/translations/pt.json | 191 ++ .../binary_sensor/translations/ro.json | 128 ++ .../binary_sensor/translations/ru.json | 191 ++ .../binary_sensor/translations/sk.json | 85 + .../binary_sensor/translations/sl.json | 189 ++ .../binary_sensor/translations/sv.json | 175 ++ .../binary_sensor/translations/ta.json | 60 + .../binary_sensor/translations/te.json | 84 + .../binary_sensor/translations/th.json | 85 + .../binary_sensor/translations/tr.json | 107 + .../binary_sensor/translations/uk.json | 191 ++ .../binary_sensor/translations/vi.json | 85 + .../binary_sensor/translations/zh-Hans.json | 191 ++ .../binary_sensor/translations/zh-Hant.json | 191 ++ .../components/bitcoin/__init__.py | 1 + .../components/bitcoin/manifest.json | 8 + .../components/bitcoin/sensor.py | 179 ++ .../components/bizkaibus/__init__.py | 1 + .../components/bizkaibus/manifest.json | 8 + .../components/bizkaibus/sensor.py | 83 + .../components/blackbird/__init__.py | 1 + .../components/blackbird/const.py | 3 + .../components/blackbird/manifest.json | 8 + .../components/blackbird/media_player.py | 216 ++ .../components/blackbird/services.yaml | 20 + .../components/blebox/__init__.py | 110 + .../components/blebox/air_quality.py | 36 + .../components/blebox/climate.py | 92 + .../components/blebox/config_flow.py | 127 ++ .../homeassistant/components/blebox/const.py | 51 + .../homeassistant/components/blebox/cover.py | 92 + .../homeassistant/components/blebox/light.py | 100 + .../components/blebox/manifest.json | 9 + .../homeassistant/components/blebox/sensor.py | 33 + .../components/blebox/strings.json | 24 + .../homeassistant/components/blebox/switch.py | 34 + .../components/blebox/translations/ca.json | 24 + .../components/blebox/translations/cs.json | 24 + .../components/blebox/translations/de.json | 24 + .../components/blebox/translations/en.json | 24 + .../blebox/translations/es-419.json | 23 + .../components/blebox/translations/es.json | 24 + .../components/blebox/translations/et.json | 24 + .../components/blebox/translations/fi.json | 11 + .../components/blebox/translations/fr.json | 24 + .../components/blebox/translations/he.json | 12 + .../components/blebox/translations/hu.json | 21 + .../components/blebox/translations/id.json | 24 + .../components/blebox/translations/it.json | 24 + .../components/blebox/translations/ko.json | 24 + .../components/blebox/translations/lb.json | 24 + .../components/blebox/translations/nl.json | 24 + .../components/blebox/translations/no.json | 24 + .../components/blebox/translations/pl.json | 0 .../homeassistant/config.py | 939 ++++++++ .../homeassistant/config_entries.py | 1494 ++++++++++++ .../homeassistant/const.py | 663 ++++++ .../homeassistant/core.py | 1787 +++++++++++++++ .../homeassistant/data_entry_flow.py | 461 ++++ .../homeassistant/exceptions.py | 201 ++ .../homeassistant/loader.py | 774 +++++++ .../homeassistant/package_constraints.txt | 61 + .../homeassistant/requirements.py | 177 ++ .../homeassistant/runner.py | 118 + .../homeassistant/setup.py | 479 ++++ .../homeassistant/strings.json | 79 + homeassistant-2021.6.0.dev0/pyproject.toml | 124 + homeassistant-2021.6.0.dev0/setup.cfg | 34 + homeassistant-2021.6.0.dev0/setup.py | 76 + homeassistant/components/wallbox/__init__.py | 14 +- .../components/wallbox/config_flow.py | 6 +- homeassistant/components/wallbox/const.py | 110 +- homeassistant/components/wallbox/sensor.py | 13 +- tests/components/wallbox/test_config_flow.py | 21 +- tests/components/wallbox/test_init.py | 15 +- tests/components/wallbox/test_sensor.py | 41 +- 1313 files changed, 85665 insertions(+), 96 deletions(-) create mode 100644 homeassistant-2021.6.0.dev0/LICENSE.md create mode 100644 homeassistant-2021.6.0.dev0/MANIFEST.in create mode 100644 homeassistant-2021.6.0.dev0/README.rst create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/__main__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/auth/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/auth/auth_store.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/auth/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/auth/mfa_modules/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/auth/mfa_modules/insecure_example.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/auth/mfa_modules/notify.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/auth/mfa_modules/totp.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/auth/models.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/entities.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/merge.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/models.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/system_policies.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/types.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/util.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/auth/providers/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/auth/providers/command_line.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/auth/providers/homeassistant.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/auth/providers/insecure_example.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/auth/providers/legacy_api_password.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/auth/providers/trusted_networks.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/block_async_io.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/bootstrap.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/alarm_control_panel.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/binary_sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/camera.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/cover.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/light.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/lock.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/services.yaml create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/switch.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/bg.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/da.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/el.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/es-419.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/fa.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/he.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/lv.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/pt-BR.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/ro.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/sl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/sv.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/th.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/strings.sensor.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/system_health.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/es-419.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/he.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/pt-BR.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.es-419.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.sv.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/zh-Hans.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/weather.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acer_projector/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acer_projector/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acer_projector/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acer_projector/switch.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/base.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/cover.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/errors.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/helpers.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/hub.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/actiontec/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/actiontec/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/actiontec/device_tracker.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/actiontec/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/actiontec/model.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/services.yaml create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/switch.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/bg.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/da.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/el.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/es-419.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/fi.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/he.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/hr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/nn.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/pt-BR.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/sl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/sv.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/vi.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/zh-Hans.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ads/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ads/binary_sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ads/cover.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ads/light.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ads/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ads/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ads/services.yaml create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ads/switch.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/binary_sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/climate.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/cover.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/entity.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/services.yaml create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/switch.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/es-419.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/ka.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/sl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/zh-Hans.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/es-419.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/weather.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aemet/weather_update_coordinator.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aftership/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aftership/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aftership/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aftership/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aftership/services.yaml create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/alarm_control_panel.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/camera.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/helpers.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/services.yaml create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/es-419.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/fi.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/he.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/ka.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/pt-BR.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/sv.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/zh-Hans.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/air_quality/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/air_quality/group.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/air_quality/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/air_quality.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/model.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/system_health.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/bg.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/da.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/el.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/es-419.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/he.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/nn.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/sl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/sv.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/zh-Hans.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/bg.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/es-419.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/air_quality.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/ar.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/bg.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/el.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/es-419.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/fi.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/he.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/hi.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/ka.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/pt-BR.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/sl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/sv.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/zh-Hans.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aladdin_connect/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aladdin_connect/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aladdin_connect/cover.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aladdin_connect/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aladdin_connect/model.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/device_action.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/device_condition.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/device_trigger.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/group.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/reproduce_state.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/services.yaml create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/af.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ar.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/bg.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/bs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/cy.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/da.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/el.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/es-419.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/eu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/fa.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/fi.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/gsw.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/he.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/hr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/hy.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/is.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ja.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/lt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/lv.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/nb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/nn.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/pt-BR.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ro.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/sk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/sl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/sv.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ta.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/te.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/th.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/vi.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/zh-Hans.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/alarm_control_panel.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/binary_sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/services.yaml create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/el.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/es-419.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/sl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/sv.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/zh-Hans.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alert/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alert/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alert/reproduce_state.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alert/services.yaml create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alexa/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alexa/auth.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alexa/capabilities.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alexa/config.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alexa/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alexa/entities.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alexa/errors.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alexa/flash_briefings.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alexa/handlers.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alexa/intent.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alexa/logbook.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alexa/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alexa/messages.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alexa/resources.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alexa/services.yaml create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alexa/smart_home.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alexa/smart_home_http.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alexa/state_report.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/bg.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/da.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/es-419.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/fi.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/pt-BR.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/sl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/sv.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alpha_vantage/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alpha_vantage/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/alpha_vantage/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/amazon_polly/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/amazon_polly/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/amazon_polly/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/amazon_polly/tts.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/climate.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/services.yaml create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/bg.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/da.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/es-419.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/ka.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/pt-BR.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/sl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/sv.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/zh-Hans.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/binary_sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/bg.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/da.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/es-419.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/fi.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/he.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/pt-BR.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/sl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/sv.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/th.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/zh-Hans.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/binary_sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/camera.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/helpers.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/services.yaml create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ampio/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ampio/air_quality.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ampio/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/ampio/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/analytics/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/analytics/analytics.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/analytics/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/analytics/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/android_ip_webcam/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/android_ip_webcam/binary_sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/android_ip_webcam/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/android_ip_webcam/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/android_ip_webcam/switch.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/androidtv/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/androidtv/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/androidtv/media_player.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/androidtv/services.yaml create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/anel_pwrctrl/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/anel_pwrctrl/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/anel_pwrctrl/switch.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/anthemav/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/anthemav/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/anthemav/media_player.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apache_kafka/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apache_kafka/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apcupsd/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apcupsd/binary_sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apcupsd/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apcupsd/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/api/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/api/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/api/services.yaml create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apns/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apns/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apns/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apns/notify.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apns/services.yaml create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/media_player.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/remote.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/sl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/zh-Hans.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apprise/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apprise/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/apprise/notify.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aprs/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aprs/device_tracker.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aprs/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aqualogic/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aqualogic/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aqualogic/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aqualogic/switch.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aquostv/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aquostv/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aquostv/media_player.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/device_trigger.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/media_player.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/pt-BR.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/ro.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/zh-Hans.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arduino/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arduino/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arduino/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arduino/switch.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arest/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arest/binary_sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arest/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arest/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arest/switch.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arlo/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arlo/alarm_control_panel.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arlo/camera.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arlo/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arlo/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arlo/services.yaml create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arris_tg2492lg/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arris_tg2492lg/device_tracker.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arris_tg2492lg/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aruba/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aruba/device_tracker.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aruba/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arwn/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arwn/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/arwn/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_cdr/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_cdr/mailbox.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_cdr/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_mbox/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_mbox/mailbox.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_mbox/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/device_tracker.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/router.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/bg.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/climate.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/es-419.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/fi.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/hi.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/ka.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/pt-BR.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/sl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/sv.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/zh-Hans.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atag/water_heater.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aten_pe/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aten_pe/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aten_pe/switch.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atome/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atome/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/atome/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/activity.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/binary_sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/camera.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/entity.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/exceptions.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/gateway.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/lock.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/subscriber.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/da.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/el.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/es-419.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/pt-BR.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/sl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/sv.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/binary_sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/ka.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/sl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/zh-Hans.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora_abb_powerone/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora_abb_powerone/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aurora_abb_powerone/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/indieauth.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/login_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/mfa_setup_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/ar.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/bg.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/da.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/es-419.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/fi.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/he.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/nn.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/pt-BR.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/ro.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/sl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/sv.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/th.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/vi.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/zh-Hans.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/blueprints/motion_light.yaml create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/config.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/helpers.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/logbook.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/reproduce_state.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/services.yaml create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/trace.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/af.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ar.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/bg.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/bs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/cy.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/da.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/el.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/es-419.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/eu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/fa.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/fi.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/gsw.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/he.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/hi.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/hr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/hy.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/is.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ja.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/lt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/lv.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/nb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/nn.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/pt-BR.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ro.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/sk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/sl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/sv.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ta.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/te.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/th.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/vi.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/zh-Hans.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/avea/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/avea/light.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/avea/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/avion/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/avion/light.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/avion/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/pt-BR.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aws/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aws/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aws/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aws/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/aws/notify.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/axis_base.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/binary_sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/camera.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/device.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/errors.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/light.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/switch.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/bg.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/da.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/es-419.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/fi.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/he.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/nn.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/pt-BR.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/sl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/sv.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/th.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/zh-Hans.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/ka.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/pt-BR.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/zh-Hans.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_event_hub/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_event_hub/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_event_hub/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_service_bus/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_service_bus/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/azure_service_bus/notify.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/baidu/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/baidu/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/baidu/tts.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/bayesian/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/bayesian/binary_sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/bayesian/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/bayesian/services.yaml create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/bbb_gpio/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/bbb_gpio/binary_sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/bbb_gpio/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/bbb_gpio/switch.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/bbox/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/bbox/device_tracker.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/bbox/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/bbox/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/beewi_smartclim/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/beewi_smartclim/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/beewi_smartclim/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/bh1750/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/bh1750/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/bh1750/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/device_condition.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/device_trigger.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/group.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/significant_change.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/af.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ar.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/bg.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/bs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/cy.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/da.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/el.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/es-419.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/eu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/fa.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/fi.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/gsw.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/he.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/hi.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/hr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/hy.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/is.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ja.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ka.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/lt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/lv.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/nb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/nn.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/pt-BR.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/pt.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ro.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ru.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/sk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/sl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/sv.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ta.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/te.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/th.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/tr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/uk.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/vi.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/zh-Hans.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/zh-Hant.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/bitcoin/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/bitcoin/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/bitcoin/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/bizkaibus/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/bizkaibus/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/bizkaibus/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blackbird/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blackbird/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blackbird/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blackbird/media_player.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blackbird/services.yaml create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/__init__.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/air_quality.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/climate.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/config_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/cover.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/light.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/manifest.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/sensor.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/strings.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/switch.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/ca.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/cs.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/de.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/en.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/es-419.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/es.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/et.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/fi.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/fr.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/he.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/hu.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/id.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/it.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/ko.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/lb.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/nl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/no.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/pl.json create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/config.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/config_entries.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/const.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/core.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/data_entry_flow.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/exceptions.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/loader.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/package_constraints.txt create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/requirements.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/runner.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/setup.py create mode 100644 homeassistant-2021.6.0.dev0/homeassistant/strings.json create mode 100644 homeassistant-2021.6.0.dev0/pyproject.toml create mode 100644 homeassistant-2021.6.0.dev0/setup.cfg create mode 100644 homeassistant-2021.6.0.dev0/setup.py diff --git a/homeassistant-2021.6.0.dev0/LICENSE.md b/homeassistant-2021.6.0.dev0/LICENSE.md new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/LICENSE.md @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/homeassistant-2021.6.0.dev0/MANIFEST.in b/homeassistant-2021.6.0.dev0/MANIFEST.in new file mode 100644 index 00000000000..490b550e705 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/MANIFEST.in @@ -0,0 +1,4 @@ +include README.rst +include LICENSE.md +graft homeassistant +recursive-exclude * *.py[co] diff --git a/homeassistant-2021.6.0.dev0/README.rst b/homeassistant-2021.6.0.dev0/README.rst new file mode 100644 index 00000000000..cf8323d2e81 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/README.rst @@ -0,0 +1,28 @@ +Home Assistant |Chat Status| +================================================================================= + +Open source home automation that puts local control and privacy first. Powered by a worldwide community of tinkerers and DIY enthusiasts. Perfect to run on a Raspberry Pi or a local server. + +Check out `home-assistant.io `__ for `a +demo `__, `installation instructions `__, +`tutorials `__ and `documentation `__. + +|screenshot-states| + +Featured integrations +--------------------- + +|screenshot-components| + +The system is built using a modular approach so support for other devices or actions can be implemented easily. See also the `section on architecture `__ and the `section on creating your own +components `__. + +If you run into issues while using Home Assistant or during development +of a component, check the `Home Assistant help section `__ of our website for further help and information. + +.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg + :target: https://discord.gg/c5DvZ4e +.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png + :target: https://home-assistant.io/demo/ +.. |screenshot-components| image:: https://raw.github.com/home-assistant/home-assistant/dev/docs/screenshot-components.png + :target: https://home-assistant.io/integrations/ diff --git a/homeassistant-2021.6.0.dev0/homeassistant/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/__init__.py new file mode 100644 index 00000000000..32da6ab0afb --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/__init__.py @@ -0,0 +1 @@ +"""Init file for Home Assistant.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/__main__.py b/homeassistant-2021.6.0.dev0/homeassistant/__main__.py new file mode 100644 index 00000000000..b01284d9974 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/__main__.py @@ -0,0 +1,322 @@ +"""Start Home Assistant.""" +from __future__ import annotations + +import argparse +import os +import platform +import subprocess +import sys +import threading + +from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__ + + +def validate_python() -> None: + """Validate that the right Python version is running.""" + if sys.version_info[:3] < REQUIRED_PYTHON_VER: + print( + "Home Assistant requires at least Python " + f"{REQUIRED_PYTHON_VER[0]}.{REQUIRED_PYTHON_VER[1]}.{REQUIRED_PYTHON_VER[2]}" + ) + sys.exit(1) + + +def ensure_config_path(config_dir: str) -> None: + """Validate the configuration directory.""" + # pylint: disable=import-outside-toplevel + import homeassistant.config as config_util + + lib_dir = os.path.join(config_dir, "deps") + + # Test if configuration directory exists + if not os.path.isdir(config_dir): + if config_dir != config_util.get_default_config_dir(): + print( + f"Fatal Error: Specified configuration directory {config_dir} " + "does not exist" + ) + sys.exit(1) + + try: + os.mkdir(config_dir) + except OSError: + print( + "Fatal Error: Unable to create default configuration " + f"directory {config_dir}" + ) + sys.exit(1) + + # Test if library directory exists + if not os.path.isdir(lib_dir): + try: + os.mkdir(lib_dir) + except OSError: + print(f"Fatal Error: Unable to create library directory {lib_dir}") + sys.exit(1) + + +def get_arguments() -> argparse.Namespace: + """Get parsed passed in arguments.""" + # pylint: disable=import-outside-toplevel + import homeassistant.config as config_util + + parser = argparse.ArgumentParser( + description="Home Assistant: Observe, Control, Automate." + ) + parser.add_argument("--version", action="version", version=__version__) + parser.add_argument( + "-c", + "--config", + metavar="path_to_config_dir", + default=config_util.get_default_config_dir(), + help="Directory that contains the Home Assistant configuration", + ) + parser.add_argument( + "--safe-mode", action="store_true", help="Start Home Assistant in safe mode" + ) + parser.add_argument( + "--debug", action="store_true", help="Start Home Assistant in debug mode" + ) + parser.add_argument( + "--open-ui", action="store_true", help="Open the webinterface in a browser" + ) + parser.add_argument( + "--skip-pip", + action="store_true", + help="Skips pip install of required packages on startup", + ) + parser.add_argument( + "-v", "--verbose", action="store_true", help="Enable verbose logging to file." + ) + parser.add_argument( + "--pid-file", + metavar="path_to_pid_file", + default=None, + help="Path to PID file useful for running as daemon", + ) + parser.add_argument( + "--log-rotate-days", + type=int, + default=None, + help="Enables daily log rotation and keeps up to the specified days", + ) + parser.add_argument( + "--log-file", + type=str, + default=None, + help="Log file to write to. If not set, CONFIG/home-assistant.log is used", + ) + parser.add_argument( + "--log-no-color", action="store_true", help="Disable color logs" + ) + parser.add_argument( + "--runner", + action="store_true", + help=f"On restart exit with code {RESTART_EXIT_CODE}", + ) + parser.add_argument( + "--script", nargs=argparse.REMAINDER, help="Run one of the embedded scripts" + ) + if os.name == "posix": + parser.add_argument( + "--daemon", action="store_true", help="Run Home Assistant as daemon" + ) + + arguments = parser.parse_args() + if os.name != "posix" or arguments.debug or arguments.runner: + setattr(arguments, "daemon", False) + + return arguments + + +def daemonize() -> None: + """Move current process to daemon process.""" + # Create first fork + pid = os.fork() + if pid > 0: + sys.exit(0) + + # Decouple fork + os.setsid() + + # Create second fork + pid = os.fork() + if pid > 0: + sys.exit(0) + + # redirect standard file descriptors to devnull + # pylint: disable=consider-using-with + infd = open(os.devnull) + outfd = open(os.devnull, "a+") + sys.stdout.flush() + sys.stderr.flush() + os.dup2(infd.fileno(), sys.stdin.fileno()) + os.dup2(outfd.fileno(), sys.stdout.fileno()) + os.dup2(outfd.fileno(), sys.stderr.fileno()) + + +def check_pid(pid_file: str) -> None: + """Check that Home Assistant is not already running.""" + # Check pid file + try: + with open(pid_file) as file: + pid = int(file.readline()) + except OSError: + # PID File does not exist + return + + # If we just restarted, we just found our own pidfile. + if pid == os.getpid(): + return + + try: + os.kill(pid, 0) + except OSError: + # PID does not exist + return + print("Fatal Error: Home Assistant is already running.") + sys.exit(1) + + +def write_pid(pid_file: str) -> None: + """Create a PID File.""" + pid = os.getpid() + try: + with open(pid_file, "w") as file: + file.write(str(pid)) + except OSError: + print(f"Fatal Error: Unable to write pid file {pid_file}") + sys.exit(1) + + +def closefds_osx(min_fd: int, max_fd: int) -> None: + """Make sure file descriptors get closed when we restart. + + We cannot call close on guarded fds, and we cannot easily test which fds + are guarded. But we can set the close-on-exec flag on everything we want to + get rid of. + """ + # pylint: disable=import-outside-toplevel + from fcntl import F_GETFD, F_SETFD, FD_CLOEXEC, fcntl + + for _fd in range(min_fd, max_fd): + try: + val = fcntl(_fd, F_GETFD) + if not val & FD_CLOEXEC: + fcntl(_fd, F_SETFD, val | FD_CLOEXEC) + except OSError: + pass + + +def cmdline() -> list[str]: + """Collect path and arguments to re-execute the current hass instance.""" + if os.path.basename(sys.argv[0]) == "__main__.py": + modulepath = os.path.dirname(sys.argv[0]) + os.environ["PYTHONPATH"] = os.path.dirname(modulepath) + return [sys.executable] + [arg for arg in sys.argv if arg != "--daemon"] + + return [arg for arg in sys.argv if arg != "--daemon"] + + +def try_to_restart() -> None: + """Attempt to clean up state and start a new Home Assistant instance.""" + # Things should be mostly shut down already at this point, now just try + # to clean up things that may have been left behind. + sys.stderr.write("Home Assistant attempting to restart.\n") + + # Count remaining threads, ideally there should only be one non-daemonized + # thread left (which is us). Nothing we really do with it, but it might be + # useful when debugging shutdown/restart issues. + try: + nthreads = sum( + thread.is_alive() and not thread.daemon for thread in threading.enumerate() + ) + if nthreads > 1: + sys.stderr.write(f"Found {nthreads} non-daemonic threads.\n") + + # Somehow we sometimes seem to trigger an assertion in the python threading + # module. It seems we find threads that have no associated OS level thread + # which are not marked as stopped at the python level. + except AssertionError: + sys.stderr.write("Failed to count non-daemonic threads.\n") + + # Try to not leave behind open filedescriptors with the emphasis on try. + try: + max_fd = os.sysconf("SC_OPEN_MAX") + except ValueError: + max_fd = 256 + + if platform.system() == "Darwin": + closefds_osx(3, max_fd) + else: + os.closerange(3, max_fd) + + # Now launch into a new instance of Home Assistant. If this fails we + # fall through and exit with error 100 (RESTART_EXIT_CODE) in which case + # systemd will restart us when RestartForceExitStatus=100 is set in the + # systemd.service file. + sys.stderr.write("Restarting Home Assistant\n") + args = cmdline() + os.execv(args[0], args) + + +def main() -> int: + """Start Home Assistant.""" + validate_python() + + # Run a simple daemon runner process on Windows to handle restarts + if os.name == "nt" and "--runner" not in sys.argv: + nt_args = cmdline() + ["--runner"] + while True: + try: + subprocess.check_call(nt_args) + sys.exit(0) + except KeyboardInterrupt: + sys.exit(0) + except subprocess.CalledProcessError as exc: + if exc.returncode != RESTART_EXIT_CODE: + sys.exit(exc.returncode) + + args = get_arguments() + + if args.script is not None: + # pylint: disable=import-outside-toplevel + from homeassistant import scripts + + return scripts.run(args.script) + + config_dir = os.path.abspath(os.path.join(os.getcwd(), args.config)) + ensure_config_path(config_dir) + + # Daemon functions + if args.pid_file: + check_pid(args.pid_file) + if args.daemon: + daemonize() + if args.pid_file: + write_pid(args.pid_file) + + # pylint: disable=import-outside-toplevel + from homeassistant import runner + + runtime_conf = runner.RuntimeConfig( + config_dir=config_dir, + verbose=args.verbose, + log_rotate_days=args.log_rotate_days, + log_file=args.log_file, + log_no_color=args.log_no_color, + skip_pip=args.skip_pip, + safe_mode=args.safe_mode, + debug=args.debug, + open_ui=args.open_ui, + ) + + exit_code = runner.run(runtime_conf) + if exit_code == RESTART_EXIT_CODE and not args.runner: + try_to_restart() + + return exit_code + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/auth/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/auth/__init__.py new file mode 100644 index 00000000000..14981d0df09 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/auth/__init__.py @@ -0,0 +1,557 @@ +"""Provide an authentication layer for Home Assistant.""" +from __future__ import annotations + +import asyncio +from collections import OrderedDict +from datetime import timedelta +from typing import Any, Dict, Mapping, Optional, Tuple, cast + +import jwt + +from homeassistant import data_entry_flow +from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.util import dt as dt_util + +from . import auth_store, models +from .const import GROUP_ID_ADMIN +from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config +from .providers import AuthProvider, LoginFlow, auth_provider_from_config + +EVENT_USER_ADDED = "user_added" +EVENT_USER_REMOVED = "user_removed" + +_MfaModuleDict = Dict[str, MultiFactorAuthModule] +_ProviderKey = Tuple[str, Optional[str]] +_ProviderDict = Dict[_ProviderKey, AuthProvider] + + +class InvalidAuthError(Exception): + """Raised when a authentication error occurs.""" + + +class InvalidProvider(Exception): + """Authentication provider not found.""" + + +async def auth_manager_from_config( + hass: HomeAssistant, + provider_configs: list[dict[str, Any]], + module_configs: list[dict[str, Any]], +) -> AuthManager: + """Initialize an auth manager from config. + + CORE_CONFIG_SCHEMA will make sure do duplicated auth providers or + mfa modules exist in configs. + """ + store = auth_store.AuthStore(hass) + if provider_configs: + providers = await asyncio.gather( + *( + auth_provider_from_config(hass, store, config) + for config in provider_configs + ) + ) + else: + providers = [] + # So returned auth providers are in same order as config + provider_hash: _ProviderDict = OrderedDict() + for provider in providers: + key = (provider.type, provider.id) + provider_hash[key] = provider + + if module_configs: + modules = await asyncio.gather( + *(auth_mfa_module_from_config(hass, config) for config in module_configs) + ) + else: + modules = [] + # So returned auth modules are in same order as config + module_hash: _MfaModuleDict = OrderedDict() + for module in modules: + module_hash[module.id] = module + + manager = AuthManager(hass, store, provider_hash, module_hash) + return manager + + +class AuthManagerFlowManager(data_entry_flow.FlowManager): + """Manage authentication flows.""" + + def __init__(self, hass: HomeAssistant, auth_manager: AuthManager): + """Init auth manager flows.""" + super().__init__(hass) + self.auth_manager = auth_manager + + async def async_create_flow( + self, + handler_key: Any, + *, + context: dict[str, Any] | None = None, + data: dict[str, Any] | None = None, + ) -> data_entry_flow.FlowHandler: + """Create a login flow.""" + auth_provider = self.auth_manager.get_auth_provider(*handler_key) + if not auth_provider: + raise KeyError(f"Unknown auth provider {handler_key}") + return await auth_provider.async_login_flow(context) + + async def async_finish_flow( + self, flow: data_entry_flow.FlowHandler, result: FlowResult + ) -> FlowResult: + """Return a user as result of login flow.""" + flow = cast(LoginFlow, flow) + + if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return result + + # we got final result + if isinstance(result["data"], models.Credentials): + result["result"] = result["data"] + return result + + auth_provider = self.auth_manager.get_auth_provider(*result["handler"]) + if not auth_provider: + raise KeyError(f"Unknown auth provider {result['handler']}") + + credentials = await auth_provider.async_get_or_create_credentials( + cast(Mapping[str, str], result["data"]), + ) + + if flow.context.get("credential_only"): + result["result"] = credentials + return result + + # multi-factor module cannot enabled for new credential + # which has not linked to a user yet + if auth_provider.support_mfa and not credentials.is_new: + user = await self.auth_manager.async_get_user_by_credentials(credentials) + if user is not None: + modules = await self.auth_manager.async_get_enabled_mfa(user) + + if modules: + flow.credential = credentials + flow.user = user + flow.available_mfa_modules = modules + return await flow.async_step_select_mfa_module() + + result["result"] = credentials + return result + + +class AuthManager: + """Manage the authentication for Home Assistant.""" + + def __init__( + self, + hass: HomeAssistant, + store: auth_store.AuthStore, + providers: _ProviderDict, + mfa_modules: _MfaModuleDict, + ) -> None: + """Initialize the auth manager.""" + self.hass = hass + self._store = store + self._providers = providers + self._mfa_modules = mfa_modules + self.login_flow = AuthManagerFlowManager(hass, self) + + @property + def auth_providers(self) -> list[AuthProvider]: + """Return a list of available auth providers.""" + return list(self._providers.values()) + + @property + def auth_mfa_modules(self) -> list[MultiFactorAuthModule]: + """Return a list of available auth modules.""" + return list(self._mfa_modules.values()) + + def get_auth_provider( + self, provider_type: str, provider_id: str | None + ) -> AuthProvider | None: + """Return an auth provider, None if not found.""" + return self._providers.get((provider_type, provider_id)) + + def get_auth_providers(self, provider_type: str) -> list[AuthProvider]: + """Return a List of auth provider of one type, Empty if not found.""" + return [ + provider + for (p_type, _), provider in self._providers.items() + if p_type == provider_type + ] + + def get_auth_mfa_module(self, module_id: str) -> MultiFactorAuthModule | None: + """Return a multi-factor auth module, None if not found.""" + return self._mfa_modules.get(module_id) + + async def async_get_users(self) -> list[models.User]: + """Retrieve all users.""" + return await self._store.async_get_users() + + async def async_get_user(self, user_id: str) -> models.User | None: + """Retrieve a user.""" + return await self._store.async_get_user(user_id) + + async def async_get_owner(self) -> models.User | None: + """Retrieve the owner.""" + users = await self.async_get_users() + return next((user for user in users if user.is_owner), None) + + async def async_get_group(self, group_id: str) -> models.Group | None: + """Retrieve all groups.""" + return await self._store.async_get_group(group_id) + + async def async_get_user_by_credentials( + self, credentials: models.Credentials + ) -> models.User | None: + """Get a user by credential, return None if not found.""" + for user in await self.async_get_users(): + for creds in user.credentials: + if creds.id == credentials.id: + return user + + return None + + async def async_create_system_user( + self, name: str, group_ids: list[str] | None = None + ) -> models.User: + """Create a system user.""" + user = await self._store.async_create_user( + name=name, system_generated=True, is_active=True, group_ids=group_ids or [] + ) + + self.hass.bus.async_fire(EVENT_USER_ADDED, {"user_id": user.id}) + + return user + + async def async_create_user( + self, name: str, group_ids: list[str] | None = None + ) -> models.User: + """Create a user.""" + kwargs: dict[str, Any] = { + "name": name, + "is_active": True, + "group_ids": group_ids or [], + } + + if await self._user_should_be_owner(): + kwargs["is_owner"] = True + + user = await self._store.async_create_user(**kwargs) + + self.hass.bus.async_fire(EVENT_USER_ADDED, {"user_id": user.id}) + + return user + + async def async_get_or_create_user( + self, credentials: models.Credentials + ) -> models.User: + """Get or create a user.""" + if not credentials.is_new: + user = await self.async_get_user_by_credentials(credentials) + if user is None: + raise ValueError("Unable to find the user.") + return user + + auth_provider = self._async_get_auth_provider(credentials) + + if auth_provider is None: + raise RuntimeError("Credential with unknown provider encountered") + + info = await auth_provider.async_user_meta_for_credentials(credentials) + + user = await self._store.async_create_user( + credentials=credentials, + name=info.name, + is_active=info.is_active, + group_ids=[GROUP_ID_ADMIN], + ) + + self.hass.bus.async_fire(EVENT_USER_ADDED, {"user_id": user.id}) + + return user + + async def async_link_user( + self, user: models.User, credentials: models.Credentials + ) -> None: + """Link credentials to an existing user.""" + await self._store.async_link_user(user, credentials) + + async def async_remove_user(self, user: models.User) -> None: + """Remove a user.""" + tasks = [ + self.async_remove_credentials(credentials) + for credentials in user.credentials + ] + + if tasks: + await asyncio.wait(tasks) + + await self._store.async_remove_user(user) + + self.hass.bus.async_fire(EVENT_USER_REMOVED, {"user_id": user.id}) + + async def async_update_user( + self, + user: models.User, + name: str | None = None, + is_active: bool | None = None, + group_ids: list[str] | None = None, + ) -> None: + """Update a user.""" + kwargs: dict[str, Any] = {} + if name is not None: + kwargs["name"] = name + if group_ids is not None: + kwargs["group_ids"] = group_ids + await self._store.async_update_user(user, **kwargs) + + if is_active is not None: + if is_active is True: + await self.async_activate_user(user) + else: + await self.async_deactivate_user(user) + + async def async_activate_user(self, user: models.User) -> None: + """Activate a user.""" + await self._store.async_activate_user(user) + + async def async_deactivate_user(self, user: models.User) -> None: + """Deactivate a user.""" + if user.is_owner: + raise ValueError("Unable to deactivate the owner") + await self._store.async_deactivate_user(user) + + async def async_remove_credentials(self, credentials: models.Credentials) -> None: + """Remove credentials.""" + provider = self._async_get_auth_provider(credentials) + + if provider is not None and hasattr(provider, "async_will_remove_credentials"): + # https://github.com/python/mypy/issues/1424 + await provider.async_will_remove_credentials(credentials) # type: ignore + + await self._store.async_remove_credentials(credentials) + + async def async_enable_user_mfa( + self, user: models.User, mfa_module_id: str, data: Any + ) -> None: + """Enable a multi-factor auth module for user.""" + if user.system_generated: + raise ValueError( + "System generated users cannot enable multi-factor auth module." + ) + + module = self.get_auth_mfa_module(mfa_module_id) + if module is None: + raise ValueError(f"Unable find multi-factor auth module: {mfa_module_id}") + + await module.async_setup_user(user.id, data) + + async def async_disable_user_mfa( + self, user: models.User, mfa_module_id: str + ) -> None: + """Disable a multi-factor auth module for user.""" + if user.system_generated: + raise ValueError( + "System generated users cannot disable multi-factor auth module." + ) + + module = self.get_auth_mfa_module(mfa_module_id) + if module is None: + raise ValueError(f"Unable find multi-factor auth module: {mfa_module_id}") + + await module.async_depose_user(user.id) + + async def async_get_enabled_mfa(self, user: models.User) -> dict[str, str]: + """List enabled mfa modules for user.""" + modules: dict[str, str] = OrderedDict() + for module_id, module in self._mfa_modules.items(): + if await module.async_is_user_setup(user.id): + modules[module_id] = module.name + return modules + + async def async_create_refresh_token( + self, + user: models.User, + client_id: str | None = None, + client_name: str | None = None, + client_icon: str | None = None, + token_type: str | None = None, + access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION, + credential: models.Credentials | None = None, + ) -> models.RefreshToken: + """Create a new refresh token for a user.""" + if not user.is_active: + raise ValueError("User is not active") + + if user.system_generated and client_id is not None: + raise ValueError( + "System generated users cannot have refresh tokens connected " + "to a client." + ) + + if token_type is None: + if user.system_generated: + token_type = models.TOKEN_TYPE_SYSTEM + else: + token_type = models.TOKEN_TYPE_NORMAL + + if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM): + raise ValueError( + "System generated users can only have system type refresh tokens" + ) + + if token_type == models.TOKEN_TYPE_NORMAL and client_id is None: + raise ValueError("Client is required to generate a refresh token.") + + if ( + token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN + and client_name is None + ): + raise ValueError("Client_name is required for long-lived access token") + + if token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN: + for token in user.refresh_tokens.values(): + if ( + token.client_name == client_name + and token.token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN + ): + # Each client_name can only have one + # long_lived_access_token type of refresh token + raise ValueError(f"{client_name} already exists") + + return await self._store.async_create_refresh_token( + user, + client_id, + client_name, + client_icon, + token_type, + access_token_expiration, + credential, + ) + + async def async_get_refresh_token( + self, token_id: str + ) -> models.RefreshToken | None: + """Get refresh token by id.""" + return await self._store.async_get_refresh_token(token_id) + + async def async_get_refresh_token_by_token( + self, token: str + ) -> models.RefreshToken | None: + """Get refresh token by token.""" + return await self._store.async_get_refresh_token_by_token(token) + + async def async_remove_refresh_token( + self, refresh_token: models.RefreshToken + ) -> None: + """Delete a refresh token.""" + await self._store.async_remove_refresh_token(refresh_token) + + @callback + def async_create_access_token( + self, refresh_token: models.RefreshToken, remote_ip: str | None = None + ) -> str: + """Create a new access token.""" + self.async_validate_refresh_token(refresh_token, remote_ip) + + self._store.async_log_refresh_token_usage(refresh_token, remote_ip) + + now = dt_util.utcnow() + return jwt.encode( + { + "iss": refresh_token.id, + "iat": now, + "exp": now + refresh_token.access_token_expiration, + }, + refresh_token.jwt_key, + algorithm="HS256", + ).decode() + + @callback + def _async_resolve_provider( + self, refresh_token: models.RefreshToken + ) -> AuthProvider | None: + """Get the auth provider for the given refresh token. + + Raises an exception if the expected provider is no longer available or return + None if no provider was expected for this refresh token. + """ + if refresh_token.credential is None: + return None + + provider = self.get_auth_provider( + refresh_token.credential.auth_provider_type, + refresh_token.credential.auth_provider_id, + ) + if provider is None: + raise InvalidProvider( + f"Auth provider {refresh_token.credential.auth_provider_type}, {refresh_token.credential.auth_provider_id} not available" + ) + return provider + + @callback + def async_validate_refresh_token( + self, refresh_token: models.RefreshToken, remote_ip: str | None = None + ) -> None: + """Validate that a refresh token is usable. + + Will raise InvalidAuthError on errors. + """ + provider = self._async_resolve_provider(refresh_token) + if provider: + provider.async_validate_refresh_token(refresh_token, remote_ip) + + async def async_validate_access_token( + self, token: str + ) -> models.RefreshToken | None: + """Return refresh token if an access token is valid.""" + try: + unverif_claims = jwt.decode(token, verify=False) + except jwt.InvalidTokenError: + return None + + refresh_token = await self.async_get_refresh_token( + cast(str, unverif_claims.get("iss")) + ) + + if refresh_token is None: + jwt_key = "" + issuer = "" + else: + jwt_key = refresh_token.jwt_key + issuer = refresh_token.id + + try: + jwt.decode(token, jwt_key, leeway=10, issuer=issuer, algorithms=["HS256"]) + except jwt.InvalidTokenError: + return None + + if refresh_token is None or not refresh_token.user.is_active: + return None + + return refresh_token + + @callback + def _async_get_auth_provider( + self, credentials: models.Credentials + ) -> AuthProvider | None: + """Get auth provider from a set of credentials.""" + auth_provider_key = ( + credentials.auth_provider_type, + credentials.auth_provider_id, + ) + return self._providers.get(auth_provider_key) + + async def _user_should_be_owner(self) -> bool: + """Determine if user should be owner. + + A user should be an owner if it is the first non-system user that is + being created. + """ + for user in await self._store.async_get_users(): + if not user.system_generated: + return False + + return True diff --git a/homeassistant-2021.6.0.dev0/homeassistant/auth/auth_store.py b/homeassistant-2021.6.0.dev0/homeassistant/auth/auth_store.py new file mode 100644 index 00000000000..0b360668ad4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/auth/auth_store.py @@ -0,0 +1,609 @@ +"""Storage for auth models.""" +from __future__ import annotations + +import asyncio +from collections import OrderedDict +from datetime import timedelta +import hmac +from logging import getLogger +from typing import Any + +from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION +from homeassistant.core import HomeAssistant, callback +from homeassistant.util import dt as dt_util + +from . import models +from .const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY, GROUP_ID_USER +from .permissions import PermissionLookup, system_policies +from .permissions.types import PolicyType + +STORAGE_VERSION = 1 +STORAGE_KEY = "auth" +GROUP_NAME_ADMIN = "Administrators" +GROUP_NAME_USER = "Users" +GROUP_NAME_READ_ONLY = "Read Only" + + +class AuthStore: + """Stores authentication info. + + Any mutation to an object should happen inside the auth store. + + The auth store is lazy. It won't load the data from disk until a method is + called that needs it. + """ + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the auth store.""" + self.hass = hass + self._users: dict[str, models.User] | None = None + self._groups: dict[str, models.Group] | None = None + self._perm_lookup: PermissionLookup | None = None + self._store = hass.helpers.storage.Store( + STORAGE_VERSION, STORAGE_KEY, private=True + ) + self._lock = asyncio.Lock() + + async def async_get_groups(self) -> list[models.Group]: + """Retrieve all users.""" + if self._groups is None: + await self._async_load() + assert self._groups is not None + + return list(self._groups.values()) + + async def async_get_group(self, group_id: str) -> models.Group | None: + """Retrieve all users.""" + if self._groups is None: + await self._async_load() + assert self._groups is not None + + return self._groups.get(group_id) + + async def async_get_users(self) -> list[models.User]: + """Retrieve all users.""" + if self._users is None: + await self._async_load() + assert self._users is not None + + return list(self._users.values()) + + async def async_get_user(self, user_id: str) -> models.User | None: + """Retrieve a user by id.""" + if self._users is None: + await self._async_load() + assert self._users is not None + + return self._users.get(user_id) + + async def async_create_user( + self, + name: str | None, + is_owner: bool | None = None, + is_active: bool | None = None, + system_generated: bool | None = None, + credentials: models.Credentials | None = None, + group_ids: list[str] | None = None, + ) -> models.User: + """Create a new user.""" + if self._users is None: + await self._async_load() + + assert self._users is not None + assert self._groups is not None + + groups = [] + for group_id in group_ids or []: + group = self._groups.get(group_id) + if group is None: + raise ValueError(f"Invalid group specified {group_id}") + groups.append(group) + + kwargs: dict[str, Any] = { + "name": name, + # Until we get group management, we just put everyone in the + # same group. + "groups": groups, + "perm_lookup": self._perm_lookup, + } + + if is_owner is not None: + kwargs["is_owner"] = is_owner + + if is_active is not None: + kwargs["is_active"] = is_active + + if system_generated is not None: + kwargs["system_generated"] = system_generated + + new_user = models.User(**kwargs) + + self._users[new_user.id] = new_user + + if credentials is None: + self._async_schedule_save() + return new_user + + # Saving is done inside the link. + await self.async_link_user(new_user, credentials) + return new_user + + async def async_link_user( + self, user: models.User, credentials: models.Credentials + ) -> None: + """Add credentials to an existing user.""" + user.credentials.append(credentials) + self._async_schedule_save() + credentials.is_new = False + + async def async_remove_user(self, user: models.User) -> None: + """Remove a user.""" + if self._users is None: + await self._async_load() + assert self._users is not None + + self._users.pop(user.id) + self._async_schedule_save() + + async def async_update_user( + self, + user: models.User, + name: str | None = None, + is_active: bool | None = None, + group_ids: list[str] | None = None, + ) -> None: + """Update a user.""" + assert self._groups is not None + + if group_ids is not None: + groups = [] + for grid in group_ids: + group = self._groups.get(grid) + if group is None: + raise ValueError("Invalid group specified.") + groups.append(group) + + user.groups = groups + user.invalidate_permission_cache() + + for attr_name, value in (("name", name), ("is_active", is_active)): + if value is not None: + setattr(user, attr_name, value) + + self._async_schedule_save() + + async def async_activate_user(self, user: models.User) -> None: + """Activate a user.""" + user.is_active = True + self._async_schedule_save() + + async def async_deactivate_user(self, user: models.User) -> None: + """Activate a user.""" + user.is_active = False + self._async_schedule_save() + + async def async_remove_credentials(self, credentials: models.Credentials) -> None: + """Remove credentials.""" + if self._users is None: + await self._async_load() + assert self._users is not None + + for user in self._users.values(): + found = None + + for index, cred in enumerate(user.credentials): + if cred is credentials: + found = index + break + + if found is not None: + user.credentials.pop(found) + break + + self._async_schedule_save() + + async def async_create_refresh_token( + self, + user: models.User, + client_id: str | None = None, + client_name: str | None = None, + client_icon: str | None = None, + token_type: str = models.TOKEN_TYPE_NORMAL, + access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION, + credential: models.Credentials | None = None, + ) -> models.RefreshToken: + """Create a new token for a user.""" + kwargs: dict[str, Any] = { + "user": user, + "client_id": client_id, + "token_type": token_type, + "access_token_expiration": access_token_expiration, + "credential": credential, + } + if client_name: + kwargs["client_name"] = client_name + if client_icon: + kwargs["client_icon"] = client_icon + + refresh_token = models.RefreshToken(**kwargs) + user.refresh_tokens[refresh_token.id] = refresh_token + + self._async_schedule_save() + return refresh_token + + async def async_remove_refresh_token( + self, refresh_token: models.RefreshToken + ) -> None: + """Remove a refresh token.""" + if self._users is None: + await self._async_load() + assert self._users is not None + + for user in self._users.values(): + if user.refresh_tokens.pop(refresh_token.id, None): + self._async_schedule_save() + break + + async def async_get_refresh_token( + self, token_id: str + ) -> models.RefreshToken | None: + """Get refresh token by id.""" + if self._users is None: + await self._async_load() + assert self._users is not None + + for user in self._users.values(): + refresh_token = user.refresh_tokens.get(token_id) + if refresh_token is not None: + return refresh_token + + return None + + async def async_get_refresh_token_by_token( + self, token: str + ) -> models.RefreshToken | None: + """Get refresh token by token.""" + if self._users is None: + await self._async_load() + assert self._users is not None + + found = None + + for user in self._users.values(): + for refresh_token in user.refresh_tokens.values(): + if hmac.compare_digest(refresh_token.token, token): + found = refresh_token + + return found + + @callback + def async_log_refresh_token_usage( + self, refresh_token: models.RefreshToken, remote_ip: str | None = None + ) -> None: + """Update refresh token last used information.""" + refresh_token.last_used_at = dt_util.utcnow() + refresh_token.last_used_ip = remote_ip + self._async_schedule_save() + + async def _async_load(self) -> None: + """Load the users.""" + async with self._lock: + if self._users is not None: + return + await self._async_load_task() + + async def _async_load_task(self) -> None: + """Load the users.""" + [ent_reg, dev_reg, data] = await asyncio.gather( + self.hass.helpers.entity_registry.async_get_registry(), + self.hass.helpers.device_registry.async_get_registry(), + self._store.async_load(), + ) + + # Make sure that we're not overriding data if 2 loads happened at the + # same time + if self._users is not None: + return + + self._perm_lookup = perm_lookup = PermissionLookup(ent_reg, dev_reg) + + if data is None: + self._set_defaults() + return + + users: dict[str, models.User] = OrderedDict() + groups: dict[str, models.Group] = OrderedDict() + credentials: dict[str, models.Credentials] = OrderedDict() + + # Soft-migrating data as we load. We are going to make sure we have a + # read only group and an admin group. There are two states that we can + # migrate from: + # 1. Data from a recent version which has a single group without policy + # 2. Data from old version which has no groups + has_admin_group = False + has_user_group = False + has_read_only_group = False + group_without_policy = None + + # When creating objects we mention each attribute explicitly. This + # prevents crashing if user rolls back HA version after a new property + # was added. + + for group_dict in data.get("groups", []): + policy: PolicyType | None = None + + if group_dict["id"] == GROUP_ID_ADMIN: + has_admin_group = True + + name = GROUP_NAME_ADMIN + policy = system_policies.ADMIN_POLICY + system_generated = True + + elif group_dict["id"] == GROUP_ID_USER: + has_user_group = True + + name = GROUP_NAME_USER + policy = system_policies.USER_POLICY + system_generated = True + + elif group_dict["id"] == GROUP_ID_READ_ONLY: + has_read_only_group = True + + name = GROUP_NAME_READ_ONLY + policy = system_policies.READ_ONLY_POLICY + system_generated = True + + else: + name = group_dict["name"] + policy = group_dict.get("policy") + system_generated = False + + # We don't want groups without a policy that are not system groups + # This is part of migrating from state 1 + if policy is None: + group_without_policy = group_dict["id"] + continue + + groups[group_dict["id"]] = models.Group( + id=group_dict["id"], + name=name, + policy=policy, + system_generated=system_generated, + ) + + # If there are no groups, add all existing users to the admin group. + # This is part of migrating from state 2 + migrate_users_to_admin_group = not groups and group_without_policy is None + + # If we find a no_policy_group, we need to migrate all users to the + # admin group. We only do this if there are no other groups, as is + # the expected state. If not expected state, not marking people admin. + # This is part of migrating from state 1 + if groups and group_without_policy is not None: + group_without_policy = None + + # This is part of migrating from state 1 and 2 + if not has_admin_group: + admin_group = _system_admin_group() + groups[admin_group.id] = admin_group + + # This is part of migrating from state 1 and 2 + if not has_read_only_group: + read_only_group = _system_read_only_group() + groups[read_only_group.id] = read_only_group + + if not has_user_group: + user_group = _system_user_group() + groups[user_group.id] = user_group + + for user_dict in data["users"]: + # Collect the users group. + user_groups = [] + for group_id in user_dict.get("group_ids", []): + # This is part of migrating from state 1 + if group_id == group_without_policy: + group_id = GROUP_ID_ADMIN + user_groups.append(groups[group_id]) + + # This is part of migrating from state 2 + if not user_dict["system_generated"] and migrate_users_to_admin_group: + user_groups.append(groups[GROUP_ID_ADMIN]) + + users[user_dict["id"]] = models.User( + name=user_dict["name"], + groups=user_groups, + id=user_dict["id"], + is_owner=user_dict["is_owner"], + is_active=user_dict["is_active"], + system_generated=user_dict["system_generated"], + perm_lookup=perm_lookup, + ) + + for cred_dict in data["credentials"]: + credential = models.Credentials( + id=cred_dict["id"], + is_new=False, + auth_provider_type=cred_dict["auth_provider_type"], + auth_provider_id=cred_dict["auth_provider_id"], + data=cred_dict["data"], + ) + credentials[cred_dict["id"]] = credential + users[cred_dict["user_id"]].credentials.append(credential) + + for rt_dict in data["refresh_tokens"]: + # Filter out the old keys that don't have jwt_key (pre-0.76) + if "jwt_key" not in rt_dict: + continue + + created_at = dt_util.parse_datetime(rt_dict["created_at"]) + if created_at is None: + getLogger(__name__).error( + "Ignoring refresh token %(id)s with invalid created_at " + "%(created_at)s for user_id %(user_id)s", + rt_dict, + ) + continue + + token_type = rt_dict.get("token_type") + if token_type is None: + if rt_dict["client_id"] is None: + token_type = models.TOKEN_TYPE_SYSTEM + else: + token_type = models.TOKEN_TYPE_NORMAL + + # old refresh_token don't have last_used_at (pre-0.78) + last_used_at_str = rt_dict.get("last_used_at") + if last_used_at_str: + last_used_at = dt_util.parse_datetime(last_used_at_str) + else: + last_used_at = None + + token = models.RefreshToken( + id=rt_dict["id"], + user=users[rt_dict["user_id"]], + client_id=rt_dict["client_id"], + # use dict.get to keep backward compatibility + client_name=rt_dict.get("client_name"), + client_icon=rt_dict.get("client_icon"), + token_type=token_type, + created_at=created_at, + access_token_expiration=timedelta( + seconds=rt_dict["access_token_expiration"] + ), + token=rt_dict["token"], + jwt_key=rt_dict["jwt_key"], + last_used_at=last_used_at, + last_used_ip=rt_dict.get("last_used_ip"), + credential=credentials.get(rt_dict.get("credential_id")), + version=rt_dict.get("version"), + ) + users[rt_dict["user_id"]].refresh_tokens[token.id] = token + + self._groups = groups + self._users = users + + @callback + def _async_schedule_save(self) -> None: + """Save users.""" + if self._users is None: + return + + self._store.async_delay_save(self._data_to_save, 1) + + @callback + def _data_to_save(self) -> dict: + """Return the data to store.""" + assert self._users is not None + assert self._groups is not None + + users = [ + { + "id": user.id, + "group_ids": [group.id for group in user.groups], + "is_owner": user.is_owner, + "is_active": user.is_active, + "name": user.name, + "system_generated": user.system_generated, + } + for user in self._users.values() + ] + + groups = [] + for group in self._groups.values(): + g_dict: dict[str, Any] = { + "id": group.id, + # Name not read for sys groups. Kept here for backwards compat + "name": group.name, + } + + if not group.system_generated: + g_dict["policy"] = group.policy + + groups.append(g_dict) + + credentials = [ + { + "id": credential.id, + "user_id": user.id, + "auth_provider_type": credential.auth_provider_type, + "auth_provider_id": credential.auth_provider_id, + "data": credential.data, + } + for user in self._users.values() + for credential in user.credentials + ] + + refresh_tokens = [ + { + "id": refresh_token.id, + "user_id": user.id, + "client_id": refresh_token.client_id, + "client_name": refresh_token.client_name, + "client_icon": refresh_token.client_icon, + "token_type": refresh_token.token_type, + "created_at": refresh_token.created_at.isoformat(), + "access_token_expiration": refresh_token.access_token_expiration.total_seconds(), + "token": refresh_token.token, + "jwt_key": refresh_token.jwt_key, + "last_used_at": refresh_token.last_used_at.isoformat() + if refresh_token.last_used_at + else None, + "last_used_ip": refresh_token.last_used_ip, + "credential_id": refresh_token.credential.id + if refresh_token.credential + else None, + "version": refresh_token.version, + } + for user in self._users.values() + for refresh_token in user.refresh_tokens.values() + ] + + return { + "users": users, + "groups": groups, + "credentials": credentials, + "refresh_tokens": refresh_tokens, + } + + def _set_defaults(self) -> None: + """Set default values for auth store.""" + self._users = OrderedDict() + + groups: dict[str, models.Group] = OrderedDict() + admin_group = _system_admin_group() + groups[admin_group.id] = admin_group + user_group = _system_user_group() + groups[user_group.id] = user_group + read_only_group = _system_read_only_group() + groups[read_only_group.id] = read_only_group + self._groups = groups + + +def _system_admin_group() -> models.Group: + """Create system admin group.""" + return models.Group( + name=GROUP_NAME_ADMIN, + id=GROUP_ID_ADMIN, + policy=system_policies.ADMIN_POLICY, + system_generated=True, + ) + + +def _system_user_group() -> models.Group: + """Create system user group.""" + return models.Group( + name=GROUP_NAME_USER, + id=GROUP_ID_USER, + policy=system_policies.USER_POLICY, + system_generated=True, + ) + + +def _system_read_only_group() -> models.Group: + """Create read only group.""" + return models.Group( + name=GROUP_NAME_READ_ONLY, + id=GROUP_ID_READ_ONLY, + policy=system_policies.READ_ONLY_POLICY, + system_generated=True, + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/auth/const.py b/homeassistant-2021.6.0.dev0/homeassistant/auth/const.py new file mode 100644 index 00000000000..5e17e752bdd --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/auth/const.py @@ -0,0 +1,9 @@ +"""Constants for the auth module.""" +from datetime import timedelta + +ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30) +MFA_SESSION_EXPIRATION = timedelta(minutes=5) + +GROUP_ID_ADMIN = "system-admin" +GROUP_ID_USER = "system-users" +GROUP_ID_READ_ONLY = "system-read-only" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/auth/mfa_modules/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/auth/mfa_modules/__init__.py new file mode 100644 index 00000000000..4adaf4776a0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/auth/mfa_modules/__init__.py @@ -0,0 +1,175 @@ +"""Pluggable auth modules for Home Assistant.""" +from __future__ import annotations + +import importlib +import logging +import types +from typing import Any + +import voluptuous as vol +from voluptuous.humanize import humanize_error + +from homeassistant import data_entry_flow, requirements +from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util.decorator import Registry + +MULTI_FACTOR_AUTH_MODULES = Registry() + +MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema( + { + vol.Required(CONF_TYPE): str, + vol.Optional(CONF_NAME): str, + # Specify ID if you have two mfa auth module for same type. + vol.Optional(CONF_ID): str, + }, + extra=vol.ALLOW_EXTRA, +) + +DATA_REQS = "mfa_auth_module_reqs_processed" + +_LOGGER = logging.getLogger(__name__) + + +class MultiFactorAuthModule: + """Multi-factor Auth Module of validation function.""" + + DEFAULT_TITLE = "Unnamed auth module" + MAX_RETRY_TIME = 3 + + def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None: + """Initialize an auth module.""" + self.hass = hass + self.config = config + + @property + def id(self) -> str: + """Return id of the auth module. + + Default is same as type + """ + return self.config.get(CONF_ID, self.type) + + @property + def type(self) -> str: + """Return type of the module.""" + return self.config[CONF_TYPE] # type: ignore + + @property + def name(self) -> str: + """Return the name of the auth module.""" + return self.config.get(CONF_NAME, self.DEFAULT_TITLE) + + # Implement by extending class + + @property + def input_schema(self) -> vol.Schema: + """Return a voluptuous schema to define mfa auth module's input.""" + raise NotImplementedError + + async def async_setup_flow(self, user_id: str) -> SetupFlow: + """Return a data entry flow handler for setup module. + + Mfa module should extend SetupFlow + """ + raise NotImplementedError + + async def async_setup_user(self, user_id: str, setup_data: Any) -> Any: + """Set up user for mfa auth module.""" + raise NotImplementedError + + async def async_depose_user(self, user_id: str) -> None: + """Remove user from mfa module.""" + raise NotImplementedError + + async def async_is_user_setup(self, user_id: str) -> bool: + """Return whether user is setup.""" + raise NotImplementedError + + async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool: + """Return True if validation passed.""" + raise NotImplementedError + + +class SetupFlow(data_entry_flow.FlowHandler): + """Handler for the setup flow.""" + + def __init__( + self, auth_module: MultiFactorAuthModule, setup_schema: vol.Schema, user_id: str + ) -> None: + """Initialize the setup flow.""" + self._auth_module = auth_module + self._setup_schema = setup_schema + self._user_id = user_id + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle the first step of setup flow. + + Return self.async_show_form(step_id='init') if user_input is None. + Return self.async_create_entry(data={'result': result}) if finish. + """ + errors: dict[str, str] = {} + + if user_input: + result = await self._auth_module.async_setup_user(self._user_id, user_input) + return self.async_create_entry( + title=self._auth_module.name, data={"result": result} + ) + + return self.async_show_form( + step_id="init", data_schema=self._setup_schema, errors=errors + ) + + +async def auth_mfa_module_from_config( + hass: HomeAssistant, config: dict[str, Any] +) -> MultiFactorAuthModule: + """Initialize an auth module from a config.""" + module_name = config[CONF_TYPE] + module = await _load_mfa_module(hass, module_name) + + try: + config = module.CONFIG_SCHEMA(config) # type: ignore + except vol.Invalid as err: + _LOGGER.error( + "Invalid configuration for multi-factor module %s: %s", + module_name, + humanize_error(config, err), + ) + raise + + return MULTI_FACTOR_AUTH_MODULES[module_name](hass, config) # type: ignore + + +async def _load_mfa_module(hass: HomeAssistant, module_name: str) -> types.ModuleType: + """Load an mfa auth module.""" + module_path = f"homeassistant.auth.mfa_modules.{module_name}" + + try: + module = importlib.import_module(module_path) + except ImportError as err: + _LOGGER.error("Unable to load mfa module %s: %s", module_name, err) + raise HomeAssistantError( + f"Unable to load mfa module {module_name}: {err}" + ) from err + + if hass.config.skip_pip or not hasattr(module, "REQUIREMENTS"): + return module + + processed = hass.data.get(DATA_REQS) + if processed and module_name in processed: + return module + + processed = hass.data[DATA_REQS] = set() + + # https://github.com/python/mypy/issues/1424 + await requirements.async_process_requirements( + hass, module_path, module.REQUIREMENTS # type: ignore + ) + + processed.add(module_name) + return module diff --git a/homeassistant-2021.6.0.dev0/homeassistant/auth/mfa_modules/insecure_example.py b/homeassistant-2021.6.0.dev0/homeassistant/auth/mfa_modules/insecure_example.py new file mode 100644 index 00000000000..1d40339417b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/auth/mfa_modules/insecure_example.py @@ -0,0 +1,87 @@ +"""Example auth module.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.core import HomeAssistant + +from . import ( + MULTI_FACTOR_AUTH_MODULE_SCHEMA, + MULTI_FACTOR_AUTH_MODULES, + MultiFactorAuthModule, + SetupFlow, +) + +CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend( + { + vol.Required("data"): [ + vol.Schema({vol.Required("user_id"): str, vol.Required("pin"): str}) + ] + }, + extra=vol.PREVENT_EXTRA, +) + + +@MULTI_FACTOR_AUTH_MODULES.register("insecure_example") +class InsecureExampleModule(MultiFactorAuthModule): + """Example auth module validate pin.""" + + DEFAULT_TITLE = "Insecure Personal Identify Number" + + def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None: + """Initialize the user data store.""" + super().__init__(hass, config) + self._data = config["data"] + + @property + def input_schema(self) -> vol.Schema: + """Validate login flow input data.""" + return vol.Schema({"pin": str}) + + @property + def setup_schema(self) -> vol.Schema: + """Validate async_setup_user input data.""" + return vol.Schema({"pin": str}) + + async def async_setup_flow(self, user_id: str) -> SetupFlow: + """Return a data entry flow handler for setup module. + + Mfa module should extend SetupFlow + """ + return SetupFlow(self, self.setup_schema, user_id) + + async def async_setup_user(self, user_id: str, setup_data: Any) -> Any: + """Set up user to use mfa module.""" + # data shall has been validate in caller + pin = setup_data["pin"] + + for data in self._data: + if data["user_id"] == user_id: + # already setup, override + data["pin"] = pin + return + + self._data.append({"user_id": user_id, "pin": pin}) + + async def async_depose_user(self, user_id: str) -> None: + """Remove user from mfa module.""" + found = None + for data in self._data: + if data["user_id"] == user_id: + found = data + break + if found: + self._data.remove(found) + + async def async_is_user_setup(self, user_id: str) -> bool: + """Return whether user is setup.""" + return any(data["user_id"] == user_id for data in self._data) + + async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool: + """Return True if validation passed.""" + return any( + data["user_id"] == user_id and data["pin"] == user_input["pin"] + for data in self._data + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/auth/mfa_modules/notify.py b/homeassistant-2021.6.0.dev0/homeassistant/auth/mfa_modules/notify.py new file mode 100644 index 00000000000..31210e2d39a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/auth/mfa_modules/notify.py @@ -0,0 +1,359 @@ +"""HMAC-based One-time Password auth module. + +Sending HOTP through notify service +""" +from __future__ import annotations + +import asyncio +from collections import OrderedDict +import logging +from typing import Any, Dict + +import attr +import voluptuous as vol + +from homeassistant.const import CONF_EXCLUDE, CONF_INCLUDE +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import ServiceNotFound +from homeassistant.helpers import config_validation as cv + +from . import ( + MULTI_FACTOR_AUTH_MODULE_SCHEMA, + MULTI_FACTOR_AUTH_MODULES, + MultiFactorAuthModule, + SetupFlow, +) + +REQUIREMENTS = ["pyotp==2.3.0"] + +CONF_MESSAGE = "message" + +CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend( + { + vol.Optional(CONF_INCLUDE): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_EXCLUDE): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_MESSAGE, default="{} is your Home Assistant login code"): str, + }, + extra=vol.PREVENT_EXTRA, +) + +STORAGE_VERSION = 1 +STORAGE_KEY = "auth_module.notify" +STORAGE_USERS = "users" +STORAGE_USER_ID = "user_id" + +INPUT_FIELD_CODE = "code" + +_LOGGER = logging.getLogger(__name__) + + +def _generate_secret() -> str: + """Generate a secret.""" + import pyotp # pylint: disable=import-outside-toplevel + + return str(pyotp.random_base32()) + + +def _generate_random() -> int: + """Generate a 8 digit number.""" + import pyotp # pylint: disable=import-outside-toplevel + + return int(pyotp.random_base32(length=8, chars=list("1234567890"))) + + +def _generate_otp(secret: str, count: int) -> str: + """Generate one time password.""" + import pyotp # pylint: disable=import-outside-toplevel + + return str(pyotp.HOTP(secret).at(count)) + + +def _verify_otp(secret: str, otp: str, count: int) -> bool: + """Verify one time password.""" + import pyotp # pylint: disable=import-outside-toplevel + + return bool(pyotp.HOTP(secret).verify(otp, count)) + + +@attr.s(slots=True) +class NotifySetting: + """Store notify setting for one user.""" + + secret: str = attr.ib(factory=_generate_secret) # not persistent + counter: int = attr.ib(factory=_generate_random) # not persistent + notify_service: str | None = attr.ib(default=None) + target: str | None = attr.ib(default=None) + + +_UsersDict = Dict[str, NotifySetting] + + +@MULTI_FACTOR_AUTH_MODULES.register("notify") +class NotifyAuthModule(MultiFactorAuthModule): + """Auth module send hmac-based one time password by notify service.""" + + DEFAULT_TITLE = "Notify One-Time Password" + + def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None: + """Initialize the user data store.""" + super().__init__(hass, config) + self._user_settings: _UsersDict | None = None + self._user_store = hass.helpers.storage.Store( + STORAGE_VERSION, STORAGE_KEY, private=True + ) + self._include = config.get(CONF_INCLUDE, []) + self._exclude = config.get(CONF_EXCLUDE, []) + self._message_template = config[CONF_MESSAGE] + self._init_lock = asyncio.Lock() + + @property + def input_schema(self) -> vol.Schema: + """Validate login flow input data.""" + return vol.Schema({INPUT_FIELD_CODE: str}) + + async def _async_load(self) -> None: + """Load stored data.""" + async with self._init_lock: + if self._user_settings is not None: + return + + data = await self._user_store.async_load() + + if data is None: + data = {STORAGE_USERS: {}} + + self._user_settings = { + user_id: NotifySetting(**setting) + for user_id, setting in data.get(STORAGE_USERS, {}).items() + } + + async def _async_save(self) -> None: + """Save data.""" + if self._user_settings is None: + return + + await self._user_store.async_save( + { + STORAGE_USERS: { + user_id: attr.asdict( + notify_setting, + filter=attr.filters.exclude( + attr.fields(NotifySetting).secret, + attr.fields(NotifySetting).counter, + ), + ) + for user_id, notify_setting in self._user_settings.items() + } + } + ) + + @callback + def aync_get_available_notify_services(self) -> list[str]: + """Return list of notify services.""" + unordered_services = set() + + for service in self.hass.services.async_services().get("notify", {}): + if service not in self._exclude: + unordered_services.add(service) + + if self._include: + unordered_services &= set(self._include) + + return sorted(unordered_services) + + async def async_setup_flow(self, user_id: str) -> SetupFlow: + """Return a data entry flow handler for setup module. + + Mfa module should extend SetupFlow + """ + return NotifySetupFlow( + self, self.input_schema, user_id, self.aync_get_available_notify_services() + ) + + async def async_setup_user(self, user_id: str, setup_data: Any) -> Any: + """Set up auth module for user.""" + if self._user_settings is None: + await self._async_load() + assert self._user_settings is not None + + self._user_settings[user_id] = NotifySetting( + notify_service=setup_data.get("notify_service"), + target=setup_data.get("target"), + ) + + await self._async_save() + + async def async_depose_user(self, user_id: str) -> None: + """Depose auth module for user.""" + if self._user_settings is None: + await self._async_load() + assert self._user_settings is not None + + if self._user_settings.pop(user_id, None): + await self._async_save() + + async def async_is_user_setup(self, user_id: str) -> bool: + """Return whether user is setup.""" + if self._user_settings is None: + await self._async_load() + assert self._user_settings is not None + + return user_id in self._user_settings + + async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool: + """Return True if validation passed.""" + if self._user_settings is None: + await self._async_load() + assert self._user_settings is not None + + notify_setting = self._user_settings.get(user_id) + if notify_setting is None: + return False + + # user_input has been validate in caller + return await self.hass.async_add_executor_job( + _verify_otp, + notify_setting.secret, + user_input.get(INPUT_FIELD_CODE, ""), + notify_setting.counter, + ) + + async def async_initialize_login_mfa_step(self, user_id: str) -> None: + """Generate code and notify user.""" + if self._user_settings is None: + await self._async_load() + assert self._user_settings is not None + + notify_setting = self._user_settings.get(user_id) + if notify_setting is None: + raise ValueError("Cannot find user_id") + + def generate_secret_and_one_time_password() -> str: + """Generate and send one time password.""" + assert notify_setting + # secret and counter are not persistent + notify_setting.secret = _generate_secret() + notify_setting.counter = _generate_random() + return _generate_otp(notify_setting.secret, notify_setting.counter) + + code = await self.hass.async_add_executor_job( + generate_secret_and_one_time_password + ) + + await self.async_notify_user(user_id, code) + + async def async_notify_user(self, user_id: str, code: str) -> None: + """Send code by user's notify service.""" + if self._user_settings is None: + await self._async_load() + assert self._user_settings is not None + + notify_setting = self._user_settings.get(user_id) + if notify_setting is None: + _LOGGER.error("Cannot find user %s", user_id) + return + + await self.async_notify( + code, + notify_setting.notify_service, # type: ignore + notify_setting.target, + ) + + async def async_notify( + self, code: str, notify_service: str, target: str | None = None + ) -> None: + """Send code by notify service.""" + data = {"message": self._message_template.format(code)} + if target: + data["target"] = [target] + + await self.hass.services.async_call("notify", notify_service, data) + + +class NotifySetupFlow(SetupFlow): + """Handler for the setup flow.""" + + def __init__( + self, + auth_module: NotifyAuthModule, + setup_schema: vol.Schema, + user_id: str, + available_notify_services: list[str], + ) -> None: + """Initialize the setup flow.""" + super().__init__(auth_module, setup_schema, user_id) + # to fix typing complaint + self._auth_module: NotifyAuthModule = auth_module + self._available_notify_services = available_notify_services + self._secret: str | None = None + self._count: int | None = None + self._notify_service: str | None = None + self._target: str | None = None + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Let user select available notify services.""" + errors: dict[str, str] = {} + + hass = self._auth_module.hass + if user_input: + self._notify_service = user_input["notify_service"] + self._target = user_input.get("target") + self._secret = await hass.async_add_executor_job(_generate_secret) + self._count = await hass.async_add_executor_job(_generate_random) + + return await self.async_step_setup() + + if not self._available_notify_services: + return self.async_abort(reason="no_available_service") + + schema: dict[str, Any] = OrderedDict() + schema["notify_service"] = vol.In(self._available_notify_services) + schema["target"] = vol.Optional(str) + + return self.async_show_form( + step_id="init", data_schema=vol.Schema(schema), errors=errors + ) + + async def async_step_setup( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Verify user can receive one-time password.""" + errors: dict[str, str] = {} + + hass = self._auth_module.hass + if user_input: + verified = await hass.async_add_executor_job( + _verify_otp, self._secret, user_input["code"], self._count + ) + if verified: + await self._auth_module.async_setup_user( + self._user_id, + {"notify_service": self._notify_service, "target": self._target}, + ) + return self.async_create_entry(title=self._auth_module.name, data={}) + + errors["base"] = "invalid_code" + + # generate code every time, no retry logic + assert self._secret and self._count + code = await hass.async_add_executor_job( + _generate_otp, self._secret, self._count + ) + + assert self._notify_service + try: + await self._auth_module.async_notify( + code, self._notify_service, self._target + ) + except ServiceNotFound: + return self.async_abort(reason="notify_service_not_exist") + + return self.async_show_form( + step_id="setup", + data_schema=self._setup_schema, + description_placeholders={"notify_service": self._notify_service}, + errors=errors, + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/auth/mfa_modules/totp.py b/homeassistant-2021.6.0.dev0/homeassistant/auth/mfa_modules/totp.py new file mode 100644 index 00000000000..20030ae166b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/auth/mfa_modules/totp.py @@ -0,0 +1,237 @@ +"""Time-based One Time Password auth module.""" +from __future__ import annotations + +import asyncio +from io import BytesIO +from typing import Any + +import voluptuous as vol + +from homeassistant.auth.models import User +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult + +from . import ( + MULTI_FACTOR_AUTH_MODULE_SCHEMA, + MULTI_FACTOR_AUTH_MODULES, + MultiFactorAuthModule, + SetupFlow, +) + +REQUIREMENTS = ["pyotp==2.3.0", "PyQRCode==1.2.1"] + +CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA) + +STORAGE_VERSION = 1 +STORAGE_KEY = "auth_module.totp" +STORAGE_USERS = "users" +STORAGE_USER_ID = "user_id" +STORAGE_OTA_SECRET = "ota_secret" + +INPUT_FIELD_CODE = "code" + +DUMMY_SECRET = "FPPTH34D4E3MI2HG" + + +def _generate_qr_code(data: str) -> str: + """Generate a base64 PNG string represent QR Code image of data.""" + import pyqrcode # pylint: disable=import-outside-toplevel + + qr_code = pyqrcode.create(data) + + with BytesIO() as buffer: + qr_code.svg(file=buffer, scale=4) + return str( + buffer.getvalue() + .decode("ascii") + .replace("\n", "") + .replace( + '' + ' tuple[str, str, str]: + """Generate a secret, url, and QR code.""" + import pyotp # pylint: disable=import-outside-toplevel + + ota_secret = pyotp.random_base32() + url = pyotp.totp.TOTP(ota_secret).provisioning_uri( + username, issuer_name="Home Assistant" + ) + image = _generate_qr_code(url) + return ota_secret, url, image + + +@MULTI_FACTOR_AUTH_MODULES.register("totp") +class TotpAuthModule(MultiFactorAuthModule): + """Auth module validate time-based one time password.""" + + DEFAULT_TITLE = "Time-based One Time Password" + MAX_RETRY_TIME = 5 + + def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None: + """Initialize the user data store.""" + super().__init__(hass, config) + self._users: dict[str, str] | None = None + self._user_store = hass.helpers.storage.Store( + STORAGE_VERSION, STORAGE_KEY, private=True + ) + self._init_lock = asyncio.Lock() + + @property + def input_schema(self) -> vol.Schema: + """Validate login flow input data.""" + return vol.Schema({INPUT_FIELD_CODE: str}) + + async def _async_load(self) -> None: + """Load stored data.""" + async with self._init_lock: + if self._users is not None: + return + + data = await self._user_store.async_load() + + if data is None: + data = {STORAGE_USERS: {}} + + self._users = data.get(STORAGE_USERS, {}) + + async def _async_save(self) -> None: + """Save data.""" + await self._user_store.async_save({STORAGE_USERS: self._users}) + + def _add_ota_secret(self, user_id: str, secret: str | None = None) -> str: + """Create a ota_secret for user.""" + import pyotp # pylint: disable=import-outside-toplevel + + ota_secret: str = secret or pyotp.random_base32() + + self._users[user_id] = ota_secret # type: ignore + return ota_secret + + async def async_setup_flow(self, user_id: str) -> SetupFlow: + """Return a data entry flow handler for setup module. + + Mfa module should extend SetupFlow + """ + user = await self.hass.auth.async_get_user(user_id) + assert user is not None + return TotpSetupFlow(self, self.input_schema, user) + + async def async_setup_user(self, user_id: str, setup_data: Any) -> str: + """Set up auth module for user.""" + if self._users is None: + await self._async_load() + + result = await self.hass.async_add_executor_job( + self._add_ota_secret, user_id, setup_data.get("secret") + ) + + await self._async_save() + return result + + async def async_depose_user(self, user_id: str) -> None: + """Depose auth module for user.""" + if self._users is None: + await self._async_load() + + if self._users.pop(user_id, None): # type: ignore + await self._async_save() + + async def async_is_user_setup(self, user_id: str) -> bool: + """Return whether user is setup.""" + if self._users is None: + await self._async_load() + + return user_id in self._users # type: ignore + + async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool: + """Return True if validation passed.""" + if self._users is None: + await self._async_load() + + # user_input has been validate in caller + # set INPUT_FIELD_CODE as vol.Required is not user friendly + return await self.hass.async_add_executor_job( + self._validate_2fa, user_id, user_input.get(INPUT_FIELD_CODE, "") + ) + + def _validate_2fa(self, user_id: str, code: str) -> bool: + """Validate two factor authentication code.""" + import pyotp # pylint: disable=import-outside-toplevel + + ota_secret = self._users.get(user_id) # type: ignore + if ota_secret is None: + # even we cannot find user, we still do verify + # to make timing the same as if user was found. + pyotp.TOTP(DUMMY_SECRET).verify(code, valid_window=1) + return False + + return bool(pyotp.TOTP(ota_secret).verify(code, valid_window=1)) + + +class TotpSetupFlow(SetupFlow): + """Handler for the setup flow.""" + + def __init__( + self, auth_module: TotpAuthModule, setup_schema: vol.Schema, user: User + ) -> None: + """Initialize the setup flow.""" + super().__init__(auth_module, setup_schema, user.id) + # to fix typing complaint + self._auth_module: TotpAuthModule = auth_module + self._user = user + self._ota_secret: str | None = None + self._url = None # type Optional[str] + self._image = None # type Optional[str] + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle the first step of setup flow. + + Return self.async_show_form(step_id='init') if user_input is None. + Return self.async_create_entry(data={'result': result}) if finish. + """ + import pyotp # pylint: disable=import-outside-toplevel + + errors: dict[str, str] = {} + + if user_input: + verified = await self.hass.async_add_executor_job( + pyotp.TOTP(self._ota_secret).verify, user_input["code"] + ) + if verified: + result = await self._auth_module.async_setup_user( + self._user_id, {"secret": self._ota_secret} + ) + return self.async_create_entry( + title=self._auth_module.name, data={"result": result} + ) + + errors["base"] = "invalid_code" + + else: + hass = self._auth_module.hass + ( + self._ota_secret, + self._url, + self._image, + ) = await hass.async_add_executor_job( + _generate_secret_and_qr_code, # type: ignore + str(self._user.name), + ) + + return self.async_show_form( + step_id="init", + data_schema=self._setup_schema, + description_placeholders={ + "code": self._ota_secret, + "url": self._url, + "qr_code": self._image, + }, + errors=errors, + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/auth/models.py b/homeassistant-2021.6.0.dev0/homeassistant/auth/models.py new file mode 100644 index 00000000000..758bbdb78e2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/auth/models.py @@ -0,0 +1,135 @@ +"""Auth models.""" +from __future__ import annotations + +from datetime import datetime, timedelta +import secrets +from typing import NamedTuple +import uuid + +import attr + +from homeassistant.const import __version__ +from homeassistant.util import dt as dt_util + +from . import permissions as perm_mdl +from .const import GROUP_ID_ADMIN + +TOKEN_TYPE_NORMAL = "normal" +TOKEN_TYPE_SYSTEM = "system" +TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token" + + +@attr.s(slots=True) +class Group: + """A group.""" + + name: str | None = attr.ib() + policy: perm_mdl.PolicyType = attr.ib() + id: str = attr.ib(factory=lambda: uuid.uuid4().hex) + system_generated: bool = attr.ib(default=False) + + +@attr.s(slots=True) +class User: + """A user.""" + + name: str | None = attr.ib() + perm_lookup: perm_mdl.PermissionLookup = attr.ib(eq=False, order=False) + id: str = attr.ib(factory=lambda: uuid.uuid4().hex) + is_owner: bool = attr.ib(default=False) + is_active: bool = attr.ib(default=False) + system_generated: bool = attr.ib(default=False) + + groups: list[Group] = attr.ib(factory=list, eq=False, order=False) + + # List of credentials of a user. + credentials: list[Credentials] = attr.ib(factory=list, eq=False, order=False) + + # Tokens associated with a user. + refresh_tokens: dict[str, RefreshToken] = attr.ib( + factory=dict, eq=False, order=False + ) + + _permissions: perm_mdl.PolicyPermissions | None = attr.ib( + init=False, + eq=False, + order=False, + default=None, + ) + + @property + def permissions(self) -> perm_mdl.AbstractPermissions: + """Return permissions object for user.""" + if self.is_owner: + return perm_mdl.OwnerPermissions + + if self._permissions is not None: + return self._permissions + + self._permissions = perm_mdl.PolicyPermissions( + perm_mdl.merge_policies([group.policy for group in self.groups]), + self.perm_lookup, + ) + + return self._permissions + + @property + def is_admin(self) -> bool: + """Return if user is part of the admin group.""" + if self.is_owner: + return True + + return self.is_active and any(gr.id == GROUP_ID_ADMIN for gr in self.groups) + + def invalidate_permission_cache(self) -> None: + """Invalidate permission cache.""" + self._permissions = None + + +@attr.s(slots=True) +class RefreshToken: + """RefreshToken for a user to grant new access tokens.""" + + user: User = attr.ib() + client_id: str | None = attr.ib() + access_token_expiration: timedelta = attr.ib() + client_name: str | None = attr.ib(default=None) + client_icon: str | None = attr.ib(default=None) + token_type: str = attr.ib( + default=TOKEN_TYPE_NORMAL, + validator=attr.validators.in_( + (TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM, TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN) + ), + ) + id: str = attr.ib(factory=lambda: uuid.uuid4().hex) + created_at: datetime = attr.ib(factory=dt_util.utcnow) + token: str = attr.ib(factory=lambda: secrets.token_hex(64)) + jwt_key: str = attr.ib(factory=lambda: secrets.token_hex(64)) + + last_used_at: datetime | None = attr.ib(default=None) + last_used_ip: str | None = attr.ib(default=None) + + credential: Credentials | None = attr.ib(default=None) + + version: str | None = attr.ib(default=__version__) + + +@attr.s(slots=True) +class Credentials: + """Credentials for a user on an auth provider.""" + + auth_provider_type: str = attr.ib() + auth_provider_id: str | None = attr.ib() + + # Allow the auth provider to store data to represent their auth. + data: dict = attr.ib() + + id: str = attr.ib(factory=lambda: uuid.uuid4().hex) + is_new: bool = attr.ib(default=True) + + +class UserMeta(NamedTuple): + """User metadata.""" + + name: str | None + is_active: bool diff --git a/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/__init__.py new file mode 100644 index 00000000000..28ff3f638d4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/__init__.py @@ -0,0 +1,77 @@ +"""Permissions for Home Assistant.""" +from __future__ import annotations + +import logging +from typing import Any, Callable + +import voluptuous as vol + +from .const import CAT_ENTITIES +from .entities import ENTITY_POLICY_SCHEMA, compile_entities +from .merge import merge_policies # noqa: F401 +from .models import PermissionLookup +from .types import PolicyType +from .util import test_all + +POLICY_SCHEMA = vol.Schema({vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA}) + +_LOGGER = logging.getLogger(__name__) + + +class AbstractPermissions: + """Default permissions class.""" + + _cached_entity_func: Callable[[str, str], bool] | None = None + + def _entity_func(self) -> Callable[[str, str], bool]: + """Return a function that can test entity access.""" + raise NotImplementedError + + def access_all_entities(self, key: str) -> bool: + """Check if we have a certain access to all entities.""" + raise NotImplementedError + + def check_entity(self, entity_id: str, key: str) -> bool: + """Check if we can access entity.""" + entity_func = self._cached_entity_func + + if entity_func is None: + entity_func = self._cached_entity_func = self._entity_func() + + return entity_func(entity_id, key) + + +class PolicyPermissions(AbstractPermissions): + """Handle permissions.""" + + def __init__(self, policy: PolicyType, perm_lookup: PermissionLookup) -> None: + """Initialize the permission class.""" + self._policy = policy + self._perm_lookup = perm_lookup + + def access_all_entities(self, key: str) -> bool: + """Check if we have a certain access to all entities.""" + return test_all(self._policy.get(CAT_ENTITIES), key) + + def _entity_func(self) -> Callable[[str, str], bool]: + """Return a function that can test entity access.""" + return compile_entities(self._policy.get(CAT_ENTITIES), self._perm_lookup) + + def __eq__(self, other: Any) -> bool: + """Equals check.""" + return isinstance(other, PolicyPermissions) and other._policy == self._policy + + +class _OwnerPermissions(AbstractPermissions): + """Owner permissions.""" + + def access_all_entities(self, key: str) -> bool: + """Check if we have a certain access to all entities.""" + return True + + def _entity_func(self) -> Callable[[str, str], bool]: + """Return a function that can test entity access.""" + return lambda entity_id, key: True + + +OwnerPermissions = _OwnerPermissions() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/const.py b/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/const.py new file mode 100644 index 00000000000..e6c44036a7e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/const.py @@ -0,0 +1,8 @@ +"""Permission constants.""" +CAT_ENTITIES = "entities" +CAT_CONFIG_ENTRIES = "config_entries" +SUBCAT_ALL = "all" + +POLICY_READ = "read" +POLICY_CONTROL = "control" +POLICY_EDIT = "edit" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/entities.py b/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/entities.py new file mode 100644 index 00000000000..f19590a6349 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/entities.py @@ -0,0 +1,100 @@ +"""Entity permissions.""" +from __future__ import annotations + +from collections import OrderedDict +from typing import Callable + +import voluptuous as vol + +from .const import POLICY_CONTROL, POLICY_EDIT, POLICY_READ, SUBCAT_ALL +from .models import PermissionLookup +from .types import CategoryType, SubCategoryDict, ValueType +from .util import SubCatLookupType, compile_policy, lookup_all + +SINGLE_ENTITY_SCHEMA = vol.Any( + True, + vol.Schema( + { + vol.Optional(POLICY_READ): True, + vol.Optional(POLICY_CONTROL): True, + vol.Optional(POLICY_EDIT): True, + } + ), +) + +ENTITY_DOMAINS = "domains" +ENTITY_AREAS = "area_ids" +ENTITY_DEVICE_IDS = "device_ids" +ENTITY_ENTITY_IDS = "entity_ids" + +ENTITY_VALUES_SCHEMA = vol.Any(True, vol.Schema({str: SINGLE_ENTITY_SCHEMA})) + +ENTITY_POLICY_SCHEMA = vol.Any( + True, + vol.Schema( + { + vol.Optional(SUBCAT_ALL): SINGLE_ENTITY_SCHEMA, + vol.Optional(ENTITY_AREAS): ENTITY_VALUES_SCHEMA, + vol.Optional(ENTITY_DEVICE_IDS): ENTITY_VALUES_SCHEMA, + vol.Optional(ENTITY_DOMAINS): ENTITY_VALUES_SCHEMA, + vol.Optional(ENTITY_ENTITY_IDS): ENTITY_VALUES_SCHEMA, + } + ), +) + + +def _lookup_domain( + perm_lookup: PermissionLookup, domains_dict: SubCategoryDict, entity_id: str +) -> ValueType | None: + """Look up entity permissions by domain.""" + return domains_dict.get(entity_id.split(".", 1)[0]) + + +def _lookup_area( + perm_lookup: PermissionLookup, area_dict: SubCategoryDict, entity_id: str +) -> ValueType | None: + """Look up entity permissions by area.""" + entity_entry = perm_lookup.entity_registry.async_get(entity_id) + + if entity_entry is None or entity_entry.device_id is None: + return None + + device_entry = perm_lookup.device_registry.async_get(entity_entry.device_id) + + if device_entry is None or device_entry.area_id is None: + return None + + return area_dict.get(device_entry.area_id) + + +def _lookup_device( + perm_lookup: PermissionLookup, devices_dict: SubCategoryDict, entity_id: str +) -> ValueType | None: + """Look up entity permissions by device.""" + entity_entry = perm_lookup.entity_registry.async_get(entity_id) + + if entity_entry is None or entity_entry.device_id is None: + return None + + return devices_dict.get(entity_entry.device_id) + + +def _lookup_entity_id( + perm_lookup: PermissionLookup, entities_dict: SubCategoryDict, entity_id: str +) -> ValueType | None: + """Look up entity permission by entity id.""" + return entities_dict.get(entity_id) + + +def compile_entities( + policy: CategoryType, perm_lookup: PermissionLookup +) -> Callable[[str, str], bool]: + """Compile policy into a function that tests policy.""" + subcategories: SubCatLookupType = OrderedDict() + subcategories[ENTITY_ENTITY_IDS] = _lookup_entity_id + subcategories[ENTITY_DEVICE_IDS] = _lookup_device + subcategories[ENTITY_AREAS] = _lookup_area + subcategories[ENTITY_DOMAINS] = _lookup_domain + subcategories[SUBCAT_ALL] = lookup_all + + return compile_policy(policy, subcategories, perm_lookup) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/merge.py b/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/merge.py new file mode 100644 index 00000000000..121d87f7848 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/merge.py @@ -0,0 +1,67 @@ +"""Merging of policies.""" +from __future__ import annotations + +from typing import cast + +from .types import CategoryType, PolicyType + + +def merge_policies(policies: list[PolicyType]) -> PolicyType: + """Merge policies.""" + new_policy: dict[str, CategoryType] = {} + seen: set[str] = set() + for policy in policies: + for category in policy: + if category in seen: + continue + seen.add(category) + new_policy[category] = _merge_policies( + [policy.get(category) for policy in policies] + ) + cast(PolicyType, new_policy) + return new_policy + + +def _merge_policies(sources: list[CategoryType]) -> CategoryType: + """Merge a policy.""" + # When merging policies, the most permissive wins. + # This means we order it like this: + # True > Dict > None + # + # True: allow everything + # Dict: specify more granular permissions + # None: no opinion + # + # If there are multiple sources with a dict as policy, we recursively + # merge each key in the source. + + policy: CategoryType = None + seen: set[str] = set() + for source in sources: + if source is None: + continue + + # A source that's True will always win. Shortcut return. + if source is True: + return True + + assert isinstance(source, dict) + + if policy is None: + policy = cast(CategoryType, {}) + + assert isinstance(policy, dict) + + for key in source: + if key in seen: + continue + seen.add(key) + + key_sources = [] + for src in sources: + if isinstance(src, dict): + key_sources.append(src.get(key)) + + policy[key] = _merge_policies(key_sources) + + return policy diff --git a/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/models.py b/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/models.py new file mode 100644 index 00000000000..aa1a777ced2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/models.py @@ -0,0 +1,20 @@ +"""Models for permissions.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import attr + +if TYPE_CHECKING: + from homeassistant.helpers import ( + device_registry as dev_reg, + entity_registry as ent_reg, + ) + + +@attr.s(slots=True) +class PermissionLookup: + """Class to hold data for permission lookups.""" + + entity_registry: ent_reg.EntityRegistry = attr.ib() + device_registry: dev_reg.DeviceRegistry = attr.ib() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/system_policies.py b/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/system_policies.py new file mode 100644 index 00000000000..b45984653fb --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/system_policies.py @@ -0,0 +1,8 @@ +"""System policies.""" +from .const import CAT_ENTITIES, POLICY_READ, SUBCAT_ALL + +ADMIN_POLICY = {CAT_ENTITIES: True} + +USER_POLICY = {CAT_ENTITIES: True} + +READ_ONLY_POLICY = {CAT_ENTITIES: {SUBCAT_ALL: {POLICY_READ: True}}} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/types.py b/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/types.py new file mode 100644 index 00000000000..6ce394ebb92 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/types.py @@ -0,0 +1,28 @@ +"""Common code for permissions.""" +from typing import Mapping, Union + +# MyPy doesn't support recursion yet. So writing it out as far as we need. + +ValueType = Union[ + # Example: entities.all = { read: true, control: true } + Mapping[str, bool], + bool, + None, +] + +# Example: entities.domains = { light: … } +SubCategoryDict = Mapping[str, ValueType] + +SubCategoryType = Union[SubCategoryDict, bool, None] + +CategoryType = Union[ + # Example: entities.domains + Mapping[str, SubCategoryType], + # Example: entities.all + Mapping[str, ValueType], + bool, + None, +] + +# Example: { entities: … } +PolicyType = Mapping[str, CategoryType] diff --git a/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/util.py b/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/util.py new file mode 100644 index 00000000000..e95e0080b50 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/auth/permissions/util.py @@ -0,0 +1,112 @@ +"""Helpers to deal with permissions.""" +from __future__ import annotations + +from functools import wraps +from typing import Callable, Dict, Optional, cast + +from .const import SUBCAT_ALL +from .models import PermissionLookup +from .types import CategoryType, SubCategoryDict, ValueType + +LookupFunc = Callable[[PermissionLookup, SubCategoryDict, str], Optional[ValueType]] +SubCatLookupType = Dict[str, LookupFunc] + + +def lookup_all( + perm_lookup: PermissionLookup, lookup_dict: SubCategoryDict, object_id: str +) -> ValueType: + """Look up permission for all.""" + # In case of ALL category, lookup_dict IS the schema. + return cast(ValueType, lookup_dict) + + +def compile_policy( + policy: CategoryType, subcategories: SubCatLookupType, perm_lookup: PermissionLookup +) -> Callable[[str, str], bool]: + """Compile policy into a function that tests policy. + + Subcategories are mapping key -> lookup function, ordered by highest + priority first. + """ + # None, False, empty dict + if not policy: + + def apply_policy_deny_all(entity_id: str, key: str) -> bool: + """Decline all.""" + return False + + return apply_policy_deny_all + + if policy is True: + + def apply_policy_allow_all(entity_id: str, key: str) -> bool: + """Approve all.""" + return True + + return apply_policy_allow_all + + assert isinstance(policy, dict) + + funcs: list[Callable[[str, str], bool | None]] = [] + + for key, lookup_func in subcategories.items(): + lookup_value = policy.get(key) + + # If any lookup value is `True`, it will always be positive + if isinstance(lookup_value, bool): + return lambda object_id, key: True + + if lookup_value is not None: + funcs.append(_gen_dict_test_func(perm_lookup, lookup_func, lookup_value)) + + if len(funcs) == 1: + func = funcs[0] + + @wraps(func) + def apply_policy_func(object_id: str, key: str) -> bool: + """Apply a single policy function.""" + return func(object_id, key) is True + + return apply_policy_func + + def apply_policy_funcs(object_id: str, key: str) -> bool: + """Apply several policy functions.""" + for func in funcs: + result = func(object_id, key) + if result is not None: + return result + return False + + return apply_policy_funcs + + +def _gen_dict_test_func( + perm_lookup: PermissionLookup, lookup_func: LookupFunc, lookup_dict: SubCategoryDict +) -> Callable[[str, str], bool | None]: + """Generate a lookup function.""" + + def test_value(object_id: str, key: str) -> bool | None: + """Test if permission is allowed based on the keys.""" + schema: ValueType = lookup_func(perm_lookup, lookup_dict, object_id) + + if schema is None or isinstance(schema, bool): + return schema + + assert isinstance(schema, dict) + + return schema.get(key) + + return test_value + + +def test_all(policy: CategoryType, key: str) -> bool: + """Test if a policy has an ALL access for a specific key.""" + if not isinstance(policy, dict): + return bool(policy) + + all_policy = policy.get(SUBCAT_ALL) + + if not isinstance(all_policy, dict): + return bool(all_policy) + + return all_policy.get(key, False) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/auth/providers/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/auth/providers/__init__.py new file mode 100644 index 00000000000..d2dfa0e1c6d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/auth/providers/__init__.py @@ -0,0 +1,292 @@ +"""Auth providers for Home Assistant.""" +from __future__ import annotations + +from collections.abc import Mapping +import importlib +import logging +import types +from typing import Any + +import voluptuous as vol +from voluptuous.humanize import humanize_error + +from homeassistant import data_entry_flow, requirements +from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util import dt as dt_util +from homeassistant.util.decorator import Registry + +from ..auth_store import AuthStore +from ..const import MFA_SESSION_EXPIRATION +from ..models import Credentials, RefreshToken, User, UserMeta + +_LOGGER = logging.getLogger(__name__) +DATA_REQS = "auth_prov_reqs_processed" + +AUTH_PROVIDERS = Registry() + +AUTH_PROVIDER_SCHEMA = vol.Schema( + { + vol.Required(CONF_TYPE): str, + vol.Optional(CONF_NAME): str, + # Specify ID if you have two auth providers for same type. + vol.Optional(CONF_ID): str, + }, + extra=vol.ALLOW_EXTRA, +) + + +class AuthProvider: + """Provider of user authentication.""" + + DEFAULT_TITLE = "Unnamed auth provider" + + def __init__( + self, hass: HomeAssistant, store: AuthStore, config: dict[str, Any] + ) -> None: + """Initialize an auth provider.""" + self.hass = hass + self.store = store + self.config = config + + @property + def id(self) -> str | None: + """Return id of the auth provider. + + Optional, can be None. + """ + return self.config.get(CONF_ID) + + @property + def type(self) -> str: + """Return type of the provider.""" + return self.config[CONF_TYPE] # type: ignore + + @property + def name(self) -> str: + """Return the name of the auth provider.""" + return self.config.get(CONF_NAME, self.DEFAULT_TITLE) + + @property + def support_mfa(self) -> bool: + """Return whether multi-factor auth supported by the auth provider.""" + return True + + async def async_credentials(self) -> list[Credentials]: + """Return all credentials of this provider.""" + users = await self.store.async_get_users() + return [ + credentials + for user in users + for credentials in user.credentials + if ( + credentials.auth_provider_type == self.type + and credentials.auth_provider_id == self.id + ) + ] + + @callback + def async_create_credentials(self, data: dict[str, str]) -> Credentials: + """Create credentials.""" + return Credentials( + auth_provider_type=self.type, auth_provider_id=self.id, data=data + ) + + # Implement by extending class + + async def async_login_flow(self, context: dict | None) -> LoginFlow: + """Return the data flow for logging in with auth provider. + + Auth provider should extend LoginFlow and return an instance. + """ + raise NotImplementedError + + async def async_get_or_create_credentials( + self, flow_result: Mapping[str, str] + ) -> Credentials: + """Get credentials based on the flow result.""" + raise NotImplementedError + + async def async_user_meta_for_credentials( + self, credentials: Credentials + ) -> UserMeta: + """Return extra user metadata for credentials. + + Will be used to populate info when creating a new user. + """ + raise NotImplementedError + + async def async_initialize(self) -> None: + """Initialize the auth provider.""" + + @callback + def async_validate_refresh_token( + self, refresh_token: RefreshToken, remote_ip: str | None = None + ) -> None: + """Verify a refresh token is still valid. + + Optional hook for an auth provider to verify validity of a refresh token. + Should raise InvalidAuthError on errors. + """ + + +async def auth_provider_from_config( + hass: HomeAssistant, store: AuthStore, config: dict[str, Any] +) -> AuthProvider: + """Initialize an auth provider from a config.""" + provider_name = config[CONF_TYPE] + module = await load_auth_provider_module(hass, provider_name) + + try: + config = module.CONFIG_SCHEMA(config) # type: ignore + except vol.Invalid as err: + _LOGGER.error( + "Invalid configuration for auth provider %s: %s", + provider_name, + humanize_error(config, err), + ) + raise + + return AUTH_PROVIDERS[provider_name](hass, store, config) # type: ignore + + +async def load_auth_provider_module( + hass: HomeAssistant, provider: str +) -> types.ModuleType: + """Load an auth provider.""" + try: + module = importlib.import_module(f"homeassistant.auth.providers.{provider}") + except ImportError as err: + _LOGGER.error("Unable to load auth provider %s: %s", provider, err) + raise HomeAssistantError( + f"Unable to load auth provider {provider}: {err}" + ) from err + + if hass.config.skip_pip or not hasattr(module, "REQUIREMENTS"): + return module + + processed = hass.data.get(DATA_REQS) + + if processed is None: + processed = hass.data[DATA_REQS] = set() + elif provider in processed: + return module + + # https://github.com/python/mypy/issues/1424 + reqs = module.REQUIREMENTS # type: ignore + await requirements.async_process_requirements( + hass, f"auth provider {provider}", reqs + ) + + processed.add(provider) + return module + + +class LoginFlow(data_entry_flow.FlowHandler): + """Handler for the login flow.""" + + def __init__(self, auth_provider: AuthProvider) -> None: + """Initialize the login flow.""" + self._auth_provider = auth_provider + self._auth_module_id: str | None = None + self._auth_manager = auth_provider.hass.auth + self.available_mfa_modules: dict[str, str] = {} + self.created_at = dt_util.utcnow() + self.invalid_mfa_times = 0 + self.user: User | None = None + self.credential: Credentials | None = None + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle the first step of login flow. + + Return self.async_show_form(step_id='init') if user_input is None. + Return await self.async_finish(flow_result) if login init step pass. + """ + raise NotImplementedError + + async def async_step_select_mfa_module( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle the step of select mfa module.""" + errors = {} + + if user_input is not None: + auth_module = user_input.get("multi_factor_auth_module") + if auth_module in self.available_mfa_modules: + self._auth_module_id = auth_module + return await self.async_step_mfa() + errors["base"] = "invalid_auth_module" + + if len(self.available_mfa_modules) == 1: + self._auth_module_id = list(self.available_mfa_modules)[0] + return await self.async_step_mfa() + + return self.async_show_form( + step_id="select_mfa_module", + data_schema=vol.Schema( + {"multi_factor_auth_module": vol.In(self.available_mfa_modules)} + ), + errors=errors, + ) + + async def async_step_mfa( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle the step of mfa validation.""" + assert self.credential + assert self.user + + errors = {} + + assert self._auth_module_id is not None + auth_module = self._auth_manager.get_auth_mfa_module(self._auth_module_id) + if auth_module is None: + # Given an invalid input to async_step_select_mfa_module + # will show invalid_auth_module error + return await self.async_step_select_mfa_module(user_input={}) + + if user_input is None and hasattr( + auth_module, "async_initialize_login_mfa_step" + ): + try: + await auth_module.async_initialize_login_mfa_step( # type: ignore + self.user.id + ) + except HomeAssistantError: + _LOGGER.exception("Error initializing MFA step") + return self.async_abort(reason="unknown_error") + + if user_input is not None: + expires = self.created_at + MFA_SESSION_EXPIRATION + if dt_util.utcnow() > expires: + return self.async_abort(reason="login_expired") + + result = await auth_module.async_validate(self.user.id, user_input) + if not result: + errors["base"] = "invalid_code" + self.invalid_mfa_times += 1 + if self.invalid_mfa_times >= auth_module.MAX_RETRY_TIME > 0: + return self.async_abort(reason="too_many_retry") + + if not errors: + return await self.async_finish(self.credential) + + description_placeholders: dict[str, str | None] = { + "mfa_module_name": auth_module.name, + "mfa_module_id": auth_module.id, + } + + return self.async_show_form( + step_id="mfa", + data_schema=auth_module.input_schema, + description_placeholders=description_placeholders, + errors=errors, + ) + + async def async_finish(self, flow_result: Any) -> FlowResult: + """Handle the pass of login flow.""" + return self.async_create_entry(title=self._auth_provider.name, data=flow_result) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/auth/providers/command_line.py b/homeassistant-2021.6.0.dev0/homeassistant/auth/providers/command_line.py new file mode 100644 index 00000000000..65d553d4eb2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/auth/providers/command_line.py @@ -0,0 +1,155 @@ +"""Auth provider that validates credentials via an external command.""" +from __future__ import annotations + +import asyncio.subprocess +import collections +from collections.abc import Mapping +import logging +import os +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.const import CONF_COMMAND +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError + +from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow +from ..models import Credentials, UserMeta + +CONF_ARGS = "args" +CONF_META = "meta" + +CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend( + { + vol.Required(CONF_COMMAND): vol.All( + str, os.path.normpath, msg="must be an absolute path" + ), + vol.Optional(CONF_ARGS, default=None): vol.Any(vol.DefaultTo(list), [str]), + vol.Optional(CONF_META, default=False): bool, + }, + extra=vol.PREVENT_EXTRA, +) + +_LOGGER = logging.getLogger(__name__) + + +class InvalidAuthError(HomeAssistantError): + """Raised when authentication with given credentials fails.""" + + +@AUTH_PROVIDERS.register("command_line") +class CommandLineAuthProvider(AuthProvider): + """Auth provider validating credentials by calling a command.""" + + DEFAULT_TITLE = "Command Line Authentication" + + # which keys to accept from a program's stdout + ALLOWED_META_KEYS = ("name",) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Extend parent's __init__. + + Adds self._user_meta dictionary to hold the user-specific + attributes provided by external programs. + """ + super().__init__(*args, **kwargs) + self._user_meta: dict[str, dict[str, Any]] = {} + + async def async_login_flow(self, context: dict | None) -> LoginFlow: + """Return a flow to login.""" + return CommandLineLoginFlow(self) + + async def async_validate_login(self, username: str, password: str) -> None: + """Validate a username and password.""" + env = {"username": username, "password": password} + try: + process = await asyncio.subprocess.create_subprocess_exec( # pylint: disable=no-member + self.config[CONF_COMMAND], + *self.config[CONF_ARGS], + env=env, + stdout=asyncio.subprocess.PIPE if self.config[CONF_META] else None, + ) + stdout, _ = await process.communicate() + except OSError as err: + # happens when command doesn't exist or permission is denied + _LOGGER.error("Error while authenticating %r: %s", username, err) + raise InvalidAuthError from err + + if process.returncode != 0: + _LOGGER.error( + "User %r failed to authenticate, command exited with code %d", + username, + process.returncode, + ) + raise InvalidAuthError + + if self.config[CONF_META]: + meta: dict[str, str] = {} + for _line in stdout.splitlines(): + try: + line = _line.decode().lstrip() + if line.startswith("#"): + continue + key, value = line.split("=", 1) + except ValueError: + # malformed line + continue + key = key.strip() + value = value.strip() + if key in self.ALLOWED_META_KEYS: + meta[key] = value + self._user_meta[username] = meta + + async def async_get_or_create_credentials( + self, flow_result: Mapping[str, str] + ) -> Credentials: + """Get credentials based on the flow result.""" + username = flow_result["username"] + for credential in await self.async_credentials(): + if credential.data["username"] == username: + return credential + + # Create new credentials. + return self.async_create_credentials({"username": username}) + + async def async_user_meta_for_credentials( + self, credentials: Credentials + ) -> UserMeta: + """Return extra user metadata for credentials. + + Currently, only name is supported. + """ + meta = self._user_meta.get(credentials.data["username"], {}) + return UserMeta(name=meta.get("name"), is_active=True) + + +class CommandLineLoginFlow(LoginFlow): + """Handler for the login flow.""" + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle the step of the form.""" + errors = {} + + if user_input is not None: + user_input["username"] = user_input["username"].strip() + try: + await cast( + CommandLineAuthProvider, self._auth_provider + ).async_validate_login(user_input["username"], user_input["password"]) + except InvalidAuthError: + errors["base"] = "invalid_auth" + + if not errors: + user_input.pop("password") + return await self.async_finish(user_input) + + schema: dict[str, type] = collections.OrderedDict() + schema["username"] = str + schema["password"] = str + + return self.async_show_form( + step_id="init", data_schema=vol.Schema(schema), errors=errors + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/auth/providers/homeassistant.py b/homeassistant-2021.6.0.dev0/homeassistant/auth/providers/homeassistant.py new file mode 100644 index 00000000000..dfbf077a89d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/auth/providers/homeassistant.py @@ -0,0 +1,346 @@ +"""Home Assistant auth provider.""" +from __future__ import annotations + +import asyncio +import base64 +from collections import OrderedDict +from collections.abc import Mapping +import logging +from typing import Any, cast + +import bcrypt +import voluptuous as vol + +from homeassistant.const import CONF_ID +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError + +from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow +from ..models import Credentials, UserMeta + +STORAGE_VERSION = 1 +STORAGE_KEY = "auth_provider.homeassistant" + + +def _disallow_id(conf: dict[str, Any]) -> dict[str, Any]: + """Disallow ID in config.""" + if CONF_ID in conf: + raise vol.Invalid("ID is not allowed for the homeassistant auth provider.") + + return conf + + +CONFIG_SCHEMA = vol.All(AUTH_PROVIDER_SCHEMA, _disallow_id) + + +@callback +def async_get_provider(hass: HomeAssistant) -> HassAuthProvider: + """Get the provider.""" + for prv in hass.auth.auth_providers: + if prv.type == "homeassistant": + return cast(HassAuthProvider, prv) + + raise RuntimeError("Provider not found") + + +class InvalidAuth(HomeAssistantError): + """Raised when we encounter invalid authentication.""" + + +class InvalidUser(HomeAssistantError): + """Raised when invalid user is specified. + + Will not be raised when validating authentication. + """ + + +class Data: + """Hold the user data.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the user data store.""" + self.hass = hass + self._store = hass.helpers.storage.Store( + STORAGE_VERSION, STORAGE_KEY, private=True + ) + self._data: dict[str, Any] | None = None + # Legacy mode will allow usernames to start/end with whitespace + # and will compare usernames case-insensitive. + # Remove in 2020 or when we launch 1.0. + self.is_legacy = False + + @callback + def normalize_username(self, username: str) -> str: + """Normalize a username based on the mode.""" + if self.is_legacy: + return username + + return username.strip().casefold() + + async def async_load(self) -> None: + """Load stored data.""" + data = await self._store.async_load() + + if data is None: + data = {"users": []} + + seen: set[str] = set() + + for user in data["users"]: + username = user["username"] + + # check if we have duplicates + folded = username.casefold() + + if folded in seen: + self.is_legacy = True + + logging.getLogger(__name__).warning( + "Home Assistant auth provider is running in legacy mode " + "because we detected usernames that are case-insensitive" + "equivalent. Please change the username: '%s'.", + username, + ) + + break + + seen.add(folded) + + # check if we have unstripped usernames + if username != username.strip(): + self.is_legacy = True + + logging.getLogger(__name__).warning( + "Home Assistant auth provider is running in legacy mode " + "because we detected usernames that start or end in a " + "space. Please change the username: '%s'.", + username, + ) + + break + + self._data = data + + @property + def users(self) -> list[dict[str, str]]: + """Return users.""" + return self._data["users"] # type: ignore + + def validate_login(self, username: str, password: str) -> None: + """Validate a username and password. + + Raises InvalidAuth if auth invalid. + """ + username = self.normalize_username(username) + dummy = b"$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO" + found = None + + # Compare all users to avoid timing attacks. + for user in self.users: + if self.normalize_username(user["username"]) == username: + found = user + + if found is None: + # check a hash to make timing the same as if user was found + bcrypt.checkpw(b"foo", dummy) + raise InvalidAuth + + user_hash = base64.b64decode(found["password"]) + + # bcrypt.checkpw is timing-safe + if not bcrypt.checkpw(password.encode(), user_hash): + raise InvalidAuth + + def hash_password( # pylint: disable=no-self-use + self, password: str, for_storage: bool = False + ) -> bytes: + """Encode a password.""" + hashed: bytes = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12)) + + if for_storage: + hashed = base64.b64encode(hashed) + return hashed + + def add_auth(self, username: str, password: str) -> None: + """Add a new authenticated user/pass.""" + username = self.normalize_username(username) + + if any( + self.normalize_username(user["username"]) == username for user in self.users + ): + raise InvalidUser + + self.users.append( + { + "username": username, + "password": self.hash_password(password, True).decode(), + } + ) + + @callback + def async_remove_auth(self, username: str) -> None: + """Remove authentication.""" + username = self.normalize_username(username) + + index = None + for i, user in enumerate(self.users): + if self.normalize_username(user["username"]) == username: + index = i + break + + if index is None: + raise InvalidUser + + self.users.pop(index) + + def change_password(self, username: str, new_password: str) -> None: + """Update the password. + + Raises InvalidUser if user cannot be found. + """ + username = self.normalize_username(username) + + for user in self.users: + if self.normalize_username(user["username"]) == username: + user["password"] = self.hash_password(new_password, True).decode() + break + else: + raise InvalidUser + + async def async_save(self) -> None: + """Save data.""" + await self._store.async_save(self._data) + + +@AUTH_PROVIDERS.register("homeassistant") +class HassAuthProvider(AuthProvider): + """Auth provider based on a local storage of users in Home Assistant config dir.""" + + DEFAULT_TITLE = "Home Assistant Local" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize an Home Assistant auth provider.""" + super().__init__(*args, **kwargs) + self.data: Data | None = None + self._init_lock = asyncio.Lock() + + async def async_initialize(self) -> None: + """Initialize the auth provider.""" + async with self._init_lock: + if self.data is not None: + return + + data = Data(self.hass) + await data.async_load() + self.data = data + + async def async_login_flow(self, context: dict | None) -> LoginFlow: + """Return a flow to login.""" + return HassLoginFlow(self) + + async def async_validate_login(self, username: str, password: str) -> None: + """Validate a username and password.""" + if self.data is None: + await self.async_initialize() + assert self.data is not None + + await self.hass.async_add_executor_job( + self.data.validate_login, username, password + ) + + async def async_add_auth(self, username: str, password: str) -> None: + """Call add_auth on data.""" + if self.data is None: + await self.async_initialize() + assert self.data is not None + + await self.hass.async_add_executor_job(self.data.add_auth, username, password) + await self.data.async_save() + + async def async_remove_auth(self, username: str) -> None: + """Call remove_auth on data.""" + if self.data is None: + await self.async_initialize() + assert self.data is not None + + self.data.async_remove_auth(username) + await self.data.async_save() + + async def async_change_password(self, username: str, new_password: str) -> None: + """Call change_password on data.""" + if self.data is None: + await self.async_initialize() + assert self.data is not None + + await self.hass.async_add_executor_job( + self.data.change_password, username, new_password + ) + await self.data.async_save() + + async def async_get_or_create_credentials( + self, flow_result: Mapping[str, str] + ) -> Credentials: + """Get credentials based on the flow result.""" + if self.data is None: + await self.async_initialize() + assert self.data is not None + + norm_username = self.data.normalize_username + username = norm_username(flow_result["username"]) + + for credential in await self.async_credentials(): + if norm_username(credential.data["username"]) == username: + return credential + + # Create new credentials. + return self.async_create_credentials({"username": username}) + + async def async_user_meta_for_credentials( + self, credentials: Credentials + ) -> UserMeta: + """Get extra info for this credential.""" + return UserMeta(name=credentials.data["username"], is_active=True) + + async def async_will_remove_credentials(self, credentials: Credentials) -> None: + """When credentials get removed, also remove the auth.""" + if self.data is None: + await self.async_initialize() + assert self.data is not None + + try: + self.data.async_remove_auth(credentials.data["username"]) + await self.data.async_save() + except InvalidUser: + # Can happen if somehow we didn't clean up a credential + pass + + +class HassLoginFlow(LoginFlow): + """Handler for the login flow.""" + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle the step of the form.""" + errors = {} + + if user_input is not None: + try: + await cast(HassAuthProvider, self._auth_provider).async_validate_login( + user_input["username"], user_input["password"] + ) + except InvalidAuth: + errors["base"] = "invalid_auth" + + if not errors: + user_input.pop("password") + return await self.async_finish(user_input) + + schema: dict[str, type] = OrderedDict() + schema["username"] = str + schema["password"] = str + + return self.async_show_form( + step_id="init", data_schema=vol.Schema(schema), errors=errors + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/auth/providers/insecure_example.py b/homeassistant-2021.6.0.dev0/homeassistant/auth/providers/insecure_example.py new file mode 100644 index 00000000000..5a3a890ff66 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/auth/providers/insecure_example.py @@ -0,0 +1,124 @@ +"""Example auth provider.""" +from __future__ import annotations + +from collections import OrderedDict +from collections.abc import Mapping +import hmac +from typing import cast + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError + +from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow +from ..models import Credentials, UserMeta + +USER_SCHEMA = vol.Schema( + { + vol.Required("username"): str, + vol.Required("password"): str, + vol.Optional("name"): str, + } +) + + +CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend( + {vol.Required("users"): [USER_SCHEMA]}, extra=vol.PREVENT_EXTRA +) + + +class InvalidAuthError(HomeAssistantError): + """Raised when submitting invalid authentication.""" + + +@AUTH_PROVIDERS.register("insecure_example") +class ExampleAuthProvider(AuthProvider): + """Example auth provider based on hardcoded usernames and passwords.""" + + async def async_login_flow(self, context: dict | None) -> LoginFlow: + """Return a flow to login.""" + return ExampleLoginFlow(self) + + @callback + def async_validate_login(self, username: str, password: str) -> None: + """Validate a username and password.""" + user = None + + # Compare all users to avoid timing attacks. + for usr in self.config["users"]: + if hmac.compare_digest( + username.encode("utf-8"), usr["username"].encode("utf-8") + ): + user = usr + + if user is None: + # Do one more compare to make timing the same as if user was found. + hmac.compare_digest(password.encode("utf-8"), password.encode("utf-8")) + raise InvalidAuthError + + if not hmac.compare_digest( + user["password"].encode("utf-8"), password.encode("utf-8") + ): + raise InvalidAuthError + + async def async_get_or_create_credentials( + self, flow_result: Mapping[str, str] + ) -> Credentials: + """Get credentials based on the flow result.""" + username = flow_result["username"] + + for credential in await self.async_credentials(): + if credential.data["username"] == username: + return credential + + # Create new credentials. + return self.async_create_credentials({"username": username}) + + async def async_user_meta_for_credentials( + self, credentials: Credentials + ) -> UserMeta: + """Return extra user metadata for credentials. + + Will be used to populate info when creating a new user. + """ + username = credentials.data["username"] + name = None + + for user in self.config["users"]: + if user["username"] == username: + name = user.get("name") + break + + return UserMeta(name=name, is_active=True) + + +class ExampleLoginFlow(LoginFlow): + """Handler for the login flow.""" + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle the step of the form.""" + errors = {} + + if user_input is not None: + try: + cast(ExampleAuthProvider, self._auth_provider).async_validate_login( + user_input["username"], user_input["password"] + ) + except InvalidAuthError: + errors["base"] = "invalid_auth" + + if not errors: + user_input.pop("password") + return await self.async_finish(user_input) + + schema: dict[str, type] = OrderedDict() + schema["username"] = str + schema["password"] = str + + return self.async_show_form( + step_id="init", data_schema=vol.Schema(schema), errors=errors + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/auth/providers/legacy_api_password.py b/homeassistant-2021.6.0.dev0/homeassistant/auth/providers/legacy_api_password.py new file mode 100644 index 00000000000..b385aa0ed59 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/auth/providers/legacy_api_password.py @@ -0,0 +1,104 @@ +""" +Support Legacy API password auth provider. + +It will be removed when auth system production ready +""" +from __future__ import annotations + +from collections.abc import Mapping +import hmac +from typing import cast + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv + +from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow +from ..models import Credentials, UserMeta + +AUTH_PROVIDER_TYPE = "legacy_api_password" +CONF_API_PASSWORD = "api_password" + +CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend( + {vol.Required(CONF_API_PASSWORD): cv.string}, extra=vol.PREVENT_EXTRA +) + +LEGACY_USER_NAME = "Legacy API password user" + + +class InvalidAuthError(HomeAssistantError): + """Raised when submitting invalid authentication.""" + + +@AUTH_PROVIDERS.register(AUTH_PROVIDER_TYPE) +class LegacyApiPasswordAuthProvider(AuthProvider): + """An auth provider support legacy api_password.""" + + DEFAULT_TITLE = "Legacy API Password" + + @property + def api_password(self) -> str: + """Return api_password.""" + return str(self.config[CONF_API_PASSWORD]) + + async def async_login_flow(self, context: dict | None) -> LoginFlow: + """Return a flow to login.""" + return LegacyLoginFlow(self) + + @callback + def async_validate_login(self, password: str) -> None: + """Validate password.""" + api_password = str(self.config[CONF_API_PASSWORD]) + + if not hmac.compare_digest( + api_password.encode("utf-8"), password.encode("utf-8") + ): + raise InvalidAuthError + + async def async_get_or_create_credentials( + self, flow_result: Mapping[str, str] + ) -> Credentials: + """Return credentials for this login.""" + credentials = await self.async_credentials() + if credentials: + return credentials[0] + + return self.async_create_credentials({}) + + async def async_user_meta_for_credentials( + self, credentials: Credentials + ) -> UserMeta: + """ + Return info for the user. + + Will be used to populate info when creating a new user. + """ + return UserMeta(name=LEGACY_USER_NAME, is_active=True) + + +class LegacyLoginFlow(LoginFlow): + """Handler for the login flow.""" + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle the step of the form.""" + errors = {} + + if user_input is not None: + try: + cast( + LegacyApiPasswordAuthProvider, self._auth_provider + ).async_validate_login(user_input["password"]) + except InvalidAuthError: + errors["base"] = "invalid_auth" + + if not errors: + return await self.async_finish({}) + + return self.async_show_form( + step_id="init", data_schema=vol.Schema({"password": str}), errors=errors + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/auth/providers/trusted_networks.py b/homeassistant-2021.6.0.dev0/homeassistant/auth/providers/trusted_networks.py new file mode 100644 index 00000000000..2f120e56652 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/auth/providers/trusted_networks.py @@ -0,0 +1,225 @@ +"""Trusted Networks auth provider. + +It shows list of users if access from trusted network. +Abort login flow if not access from trusted network. +""" +from __future__ import annotations + +from collections.abc import Mapping +from ipaddress import ( + IPv4Address, + IPv4Network, + IPv6Address, + IPv6Network, + ip_address, + ip_network, +) +from typing import Any, Dict, List, Union, cast + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv + +from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow +from .. import InvalidAuthError +from ..models import Credentials, RefreshToken, UserMeta + +IPAddress = Union[IPv4Address, IPv6Address] +IPNetwork = Union[IPv4Network, IPv6Network] + +CONF_TRUSTED_NETWORKS = "trusted_networks" +CONF_TRUSTED_USERS = "trusted_users" +CONF_GROUP = "group" +CONF_ALLOW_BYPASS_LOGIN = "allow_bypass_login" + +CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend( + { + vol.Required(CONF_TRUSTED_NETWORKS): vol.All(cv.ensure_list, [ip_network]), + vol.Optional(CONF_TRUSTED_USERS, default={}): vol.Schema( + # we only validate the format of user_id or group_id + { + ip_network: vol.All( + cv.ensure_list, + [ + vol.Or( + cv.uuid4_hex, + vol.Schema({vol.Required(CONF_GROUP): cv.uuid4_hex}), + ) + ], + ) + } + ), + vol.Optional(CONF_ALLOW_BYPASS_LOGIN, default=False): cv.boolean, + }, + extra=vol.PREVENT_EXTRA, +) + + +class InvalidUserError(HomeAssistantError): + """Raised when try to login as invalid user.""" + + +@AUTH_PROVIDERS.register("trusted_networks") +class TrustedNetworksAuthProvider(AuthProvider): + """Trusted Networks auth provider. + + Allow passwordless access from trusted network. + """ + + DEFAULT_TITLE = "Trusted Networks" + + @property + def trusted_networks(self) -> list[IPNetwork]: + """Return trusted networks.""" + return cast(List[IPNetwork], self.config[CONF_TRUSTED_NETWORKS]) + + @property + def trusted_users(self) -> dict[IPNetwork, Any]: + """Return trusted users per network.""" + return cast(Dict[IPNetwork, Any], self.config[CONF_TRUSTED_USERS]) + + @property + def support_mfa(self) -> bool: + """Trusted Networks auth provider does not support MFA.""" + return False + + async def async_login_flow(self, context: dict | None) -> LoginFlow: + """Return a flow to login.""" + assert context is not None + ip_addr = cast(IPAddress, context.get("ip_address")) + users = await self.store.async_get_users() + available_users = [ + user for user in users if not user.system_generated and user.is_active + ] + for ip_net, user_or_group_list in self.trusted_users.items(): + if ip_addr in ip_net: + user_list = [ + user_id + for user_id in user_or_group_list + if isinstance(user_id, str) + ] + group_list = [ + group[CONF_GROUP] + for group in user_or_group_list + if isinstance(group, dict) + ] + flattened_group_list = [ + group for sublist in group_list for group in sublist + ] + available_users = [ + user + for user in available_users + if ( + user.id in user_list + or any( + group.id in flattened_group_list for group in user.groups + ) + ) + ] + break + + return TrustedNetworksLoginFlow( + self, + ip_addr, + {user.id: user.name for user in available_users}, + self.config[CONF_ALLOW_BYPASS_LOGIN], + ) + + async def async_get_or_create_credentials( + self, flow_result: Mapping[str, str] + ) -> Credentials: + """Get credentials based on the flow result.""" + user_id = flow_result["user"] + + users = await self.store.async_get_users() + for user in users: + if not user.system_generated and user.is_active and user.id == user_id: + for credential in await self.async_credentials(): + if credential.data["user_id"] == user_id: + return credential + cred = self.async_create_credentials({"user_id": user_id}) + await self.store.async_link_user(user, cred) + return cred + + # We only allow login as exist user + raise InvalidUserError + + async def async_user_meta_for_credentials( + self, credentials: Credentials + ) -> UserMeta: + """Return extra user metadata for credentials. + + Trusted network auth provider should never create new user. + """ + raise NotImplementedError + + @callback + def async_validate_access(self, ip_addr: IPAddress) -> None: + """Make sure the access from trusted networks. + + Raise InvalidAuthError if not. + Raise InvalidAuthError if trusted_networks is not configured. + """ + if not self.trusted_networks: + raise InvalidAuthError("trusted_networks is not configured") + + if not any( + ip_addr in trusted_network for trusted_network in self.trusted_networks + ): + raise InvalidAuthError("Not in trusted_networks") + + @callback + def async_validate_refresh_token( + self, refresh_token: RefreshToken, remote_ip: str | None = None + ) -> None: + """Verify a refresh token is still valid.""" + if remote_ip is None: + raise InvalidAuthError( + "Unknown remote ip can't be used for trusted network provider." + ) + self.async_validate_access(ip_address(remote_ip)) + + +class TrustedNetworksLoginFlow(LoginFlow): + """Handler for the login flow.""" + + def __init__( + self, + auth_provider: TrustedNetworksAuthProvider, + ip_addr: IPAddress, + available_users: dict[str, str | None], + allow_bypass_login: bool, + ) -> None: + """Initialize the login flow.""" + super().__init__(auth_provider) + self._available_users = available_users + self._ip_address = ip_addr + self._allow_bypass_login = allow_bypass_login + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle the step of the form.""" + try: + cast( + TrustedNetworksAuthProvider, self._auth_provider + ).async_validate_access(self._ip_address) + + except InvalidAuthError: + return self.async_abort(reason="not_allowed") + + if user_input is not None: + return await self.async_finish(user_input) + + if self._allow_bypass_login and len(self._available_users) == 1: + return await self.async_finish( + {"user": next(iter(self._available_users.keys()))} + ) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema({"user": vol.In(self._available_users)}), + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/block_async_io.py b/homeassistant-2021.6.0.dev0/homeassistant/block_async_io.py new file mode 100644 index 00000000000..ec56b746706 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/block_async_io.py @@ -0,0 +1,14 @@ +"""Block I/O being done in asyncio.""" +from http.client import HTTPConnection + +from homeassistant.util.async_ import protect_loop + + +def enable() -> None: + """Enable the detection of I/O in the event loop.""" + # Prevent urllib3 and requests doing I/O in event loop + HTTPConnection.putrequest = protect_loop(HTTPConnection.putrequest) # type: ignore + + # Currently disabled. pytz doing I/O when getting timezone. + # Prevent files being opened inside the event loop + # builtins.open = protect_loop(builtins.open) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/bootstrap.py b/homeassistant-2021.6.0.dev0/homeassistant/bootstrap.py new file mode 100644 index 00000000000..45c04651461 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/bootstrap.py @@ -0,0 +1,580 @@ +"""Provide methods to bootstrap a Home Assistant instance.""" +from __future__ import annotations + +import asyncio +import contextlib +from datetime import datetime +import logging +import logging.handlers +import os +import sys +import threading +from time import monotonic +from typing import TYPE_CHECKING, Any + +import voluptuous as vol +import yarl + +from homeassistant import config as conf_util, config_entries, core, loader +from homeassistant.components import http +from homeassistant.const import REQUIRED_NEXT_PYTHON_DATE, REQUIRED_NEXT_PYTHON_VER +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import area_registry, device_registry, entity_registry +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.typing import ConfigType +from homeassistant.setup import ( + DATA_SETUP, + DATA_SETUP_STARTED, + DATA_SETUP_TIME, + async_set_domains_to_be_loaded, + async_setup_component, +) +from homeassistant.util.async_ import gather_with_concurrency +import homeassistant.util.dt as dt_util +from homeassistant.util.logging import async_activate_log_queue_handler +from homeassistant.util.package import async_get_user_site, is_virtual_env + +if TYPE_CHECKING: + from .runner import RuntimeConfig + +_LOGGER = logging.getLogger(__name__) + +ERROR_LOG_FILENAME = "home-assistant.log" + +# hass.data key for logging information. +DATA_LOGGING = "logging" + +LOG_SLOW_STARTUP_INTERVAL = 60 +SLOW_STARTUP_CHECK_INTERVAL = 1 +SIGNAL_BOOTSTRAP_INTEGRATONS = "bootstrap_integrations" + +STAGE_1_TIMEOUT = 120 +STAGE_2_TIMEOUT = 300 +WRAP_UP_TIMEOUT = 300 +COOLDOWN_TIME = 60 + +MAX_LOAD_CONCURRENTLY = 6 + +DEBUGGER_INTEGRATIONS = {"debugpy"} +CORE_INTEGRATIONS = ("homeassistant", "persistent_notification") +LOGGING_INTEGRATIONS = { + # Set log levels + "logger", + # Error logging + "system_log", + "sentry", + # To record data + "recorder", +} +STAGE_1_INTEGRATIONS = { + # To make sure we forward data to other instances + "mqtt_eventstream", + # To provide account link implementations + "cloud", + # Ensure supervisor is available + "hassio", + # Get the frontend up and running as soon + # as possible so problem integrations can + # be removed + "frontend", +} + + +async def async_setup_hass( + runtime_config: RuntimeConfig, +) -> core.HomeAssistant | None: + """Set up Home Assistant.""" + hass = core.HomeAssistant() + hass.config.config_dir = runtime_config.config_dir + + async_enable_logging( + hass, + runtime_config.verbose, + runtime_config.log_rotate_days, + runtime_config.log_file, + runtime_config.log_no_color, + ) + + hass.config.skip_pip = runtime_config.skip_pip + if runtime_config.skip_pip: + _LOGGER.warning( + "Skipping pip installation of required modules. This may cause issues" + ) + + if not await conf_util.async_ensure_config_exists(hass): + _LOGGER.error("Error getting configuration path") + return None + + _LOGGER.info("Config directory: %s", runtime_config.config_dir) + + config_dict = None + basic_setup_success = False + safe_mode = runtime_config.safe_mode + + if not safe_mode: + await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass) + + try: + config_dict = await conf_util.async_hass_config_yaml(hass) + except HomeAssistantError as err: + _LOGGER.error( + "Failed to parse configuration.yaml: %s. Activating safe mode", + err, + ) + else: + if not is_virtual_env(): + await async_mount_local_lib_path(runtime_config.config_dir) + + basic_setup_success = ( + await async_from_config_dict(config_dict, hass) is not None + ) + + if config_dict is None: + safe_mode = True + + elif not basic_setup_success: + _LOGGER.warning("Unable to set up core integrations. Activating safe mode") + safe_mode = True + + elif ( + "frontend" in hass.data.get(DATA_SETUP, {}) + and "frontend" not in hass.config.components + ): + _LOGGER.warning("Detected that frontend did not load. Activating safe mode") + # Ask integrations to shut down. It's messy but we can't + # do a clean stop without knowing what is broken + with contextlib.suppress(asyncio.TimeoutError): + async with hass.timeout.async_timeout(10): + await hass.async_stop() + + safe_mode = True + old_config = hass.config + + hass = core.HomeAssistant() + hass.config.skip_pip = old_config.skip_pip + hass.config.internal_url = old_config.internal_url + hass.config.external_url = old_config.external_url + hass.config.config_dir = old_config.config_dir + + if safe_mode: + _LOGGER.info("Starting in safe mode") + hass.config.safe_mode = True + + http_conf = (await http.async_get_last_config(hass)) or {} + + await async_from_config_dict( + {"safe_mode": {}, "http": http_conf}, + hass, + ) + + if runtime_config.open_ui: + hass.add_job(open_hass_ui, hass) + + return hass + + +def open_hass_ui(hass: core.HomeAssistant) -> None: + """Open the UI.""" + import webbrowser # pylint: disable=import-outside-toplevel + + if hass.config.api is None or "frontend" not in hass.config.components: + _LOGGER.warning("Cannot launch the UI because frontend not loaded") + return + + scheme = "https" if hass.config.api.use_ssl else "http" + url = str( + yarl.URL.build(scheme=scheme, host="127.0.0.1", port=hass.config.api.port) + ) + + if not webbrowser.open(url): + _LOGGER.warning( + "Unable to open the Home Assistant UI in a browser. Open it yourself at %s", + url, + ) + + +async def async_from_config_dict( + config: ConfigType, hass: core.HomeAssistant +) -> core.HomeAssistant | None: + """Try to configure Home Assistant from a configuration dictionary. + + Dynamically loads required components and its dependencies. + This method is a coroutine. + """ + start = monotonic() + + hass.config_entries = config_entries.ConfigEntries(hass, config) + await hass.config_entries.async_initialize() + + # Set up core. + _LOGGER.debug("Setting up %s", CORE_INTEGRATIONS) + + if not all( + await asyncio.gather( + *( + async_setup_component(hass, domain, config) + for domain in CORE_INTEGRATIONS + ) + ) + ): + _LOGGER.error("Home Assistant core failed to initialize. ") + return None + + _LOGGER.debug("Home Assistant core initialized") + + core_config = config.get(core.DOMAIN, {}) + + try: + await conf_util.async_process_ha_core_config(hass, core_config) + except vol.Invalid as config_err: + conf_util.async_log_exception(config_err, "homeassistant", core_config, hass) + return None + except HomeAssistantError: + _LOGGER.error( + "Home Assistant core failed to initialize. " + "Further initialization aborted" + ) + return None + + await _async_set_up_integrations(hass, config) + + stop = monotonic() + _LOGGER.info("Home Assistant initialized in %.2fs", stop - start) + + if REQUIRED_NEXT_PYTHON_DATE and sys.version_info[:3] < REQUIRED_NEXT_PYTHON_VER: + msg = ( + "Support for the running Python version " + f"{'.'.join(str(x) for x in sys.version_info[:3])} is deprecated and will " + f"be removed in the first release after {REQUIRED_NEXT_PYTHON_DATE}. " + "Please upgrade Python to " + f"{'.'.join(str(x) for x in REQUIRED_NEXT_PYTHON_VER)} or " + "higher." + ) + _LOGGER.warning(msg) + hass.components.persistent_notification.async_create( + msg, "Python version", "python_version" + ) + + return hass + + +@core.callback +def async_enable_logging( + hass: core.HomeAssistant, + verbose: bool = False, + log_rotate_days: int | None = None, + log_file: str | None = None, + log_no_color: bool = False, +) -> None: + """Set up the logging. + + This method must be run in the event loop. + """ + fmt = "%(asctime)s %(levelname)s (%(threadName)s) [%(name)s] %(message)s" + datefmt = "%Y-%m-%d %H:%M:%S" + + if not log_no_color: + try: + # pylint: disable=import-outside-toplevel + from colorlog import ColoredFormatter + + # basicConfig must be called after importing colorlog in order to + # ensure that the handlers it sets up wraps the correct streams. + logging.basicConfig(level=logging.INFO) + + colorfmt = f"%(log_color)s{fmt}%(reset)s" + logging.getLogger().handlers[0].setFormatter( + ColoredFormatter( + colorfmt, + datefmt=datefmt, + reset=True, + log_colors={ + "DEBUG": "cyan", + "INFO": "green", + "WARNING": "yellow", + "ERROR": "red", + "CRITICAL": "red", + }, + ) + ) + except ImportError: + pass + + # If the above initialization failed for any reason, setup the default + # formatting. If the above succeeds, this will result in a no-op. + logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO) + + # Suppress overly verbose logs from libraries that aren't helpful + logging.getLogger("requests").setLevel(logging.WARNING) + logging.getLogger("urllib3").setLevel(logging.WARNING) + logging.getLogger("aiohttp.access").setLevel(logging.WARNING) + + sys.excepthook = lambda *args: logging.getLogger(None).exception( + "Uncaught exception", exc_info=args # type: ignore + ) + threading.excepthook = lambda args: logging.getLogger(None).exception( + "Uncaught thread exception", + exc_info=(args.exc_type, args.exc_value, args.exc_traceback), # type: ignore[arg-type] + ) + + # Log errors to a file if we have write access to file or config dir + if log_file is None: + err_log_path = hass.config.path(ERROR_LOG_FILENAME) + else: + err_log_path = os.path.abspath(log_file) + + err_path_exists = os.path.isfile(err_log_path) + err_dir = os.path.dirname(err_log_path) + + # Check if we can write to the error log if it exists or that + # we can create files in the containing directory if not. + if (err_path_exists and os.access(err_log_path, os.W_OK)) or ( + not err_path_exists and os.access(err_dir, os.W_OK) + ): + + if log_rotate_days: + err_handler: logging.FileHandler = ( + logging.handlers.TimedRotatingFileHandler( + err_log_path, when="midnight", backupCount=log_rotate_days + ) + ) + else: + err_handler = logging.FileHandler(err_log_path, mode="w", delay=True) + + err_handler.setLevel(logging.INFO if verbose else logging.WARNING) + err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt)) + + logger = logging.getLogger("") + logger.addHandler(err_handler) + logger.setLevel(logging.INFO if verbose else logging.WARNING) + + # Save the log file location for access by other components. + hass.data[DATA_LOGGING] = err_log_path + else: + _LOGGER.error("Unable to set up error log %s (access denied)", err_log_path) + + async_activate_log_queue_handler(hass) + + +async def async_mount_local_lib_path(config_dir: str) -> str: + """Add local library to Python Path. + + This function is a coroutine. + """ + deps_dir = os.path.join(config_dir, "deps") + lib_dir = await async_get_user_site(deps_dir) + if lib_dir not in sys.path: + sys.path.insert(0, lib_dir) + return deps_dir + + +@core.callback +def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]: + """Get domains of components to set up.""" + # Filter out the repeating and common config section [homeassistant] + domains = {key.split(" ")[0] for key in config if key != core.DOMAIN} + + # Add config entry domains + if not hass.config.safe_mode: + domains.update(hass.config_entries.async_domains()) + + # Make sure the Hass.io component is loaded + if "HASSIO" in os.environ: + domains.add("hassio") + + return domains + + +async def _async_watch_pending_setups(hass: core.HomeAssistant) -> None: + """Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL.""" + loop_count = 0 + setup_started: dict[str, datetime] = hass.data[DATA_SETUP_STARTED] + previous_was_empty = True + while True: + now = dt_util.utcnow() + remaining_with_setup_started = { + domain: (now - setup_started[domain]).total_seconds() + for domain in setup_started + } + _LOGGER.debug("Integration remaining: %s", remaining_with_setup_started) + if remaining_with_setup_started or not previous_was_empty: + async_dispatcher_send( + hass, SIGNAL_BOOTSTRAP_INTEGRATONS, remaining_with_setup_started + ) + previous_was_empty = not remaining_with_setup_started + await asyncio.sleep(SLOW_STARTUP_CHECK_INTERVAL) + loop_count += SLOW_STARTUP_CHECK_INTERVAL + + if loop_count >= LOG_SLOW_STARTUP_INTERVAL and setup_started: + _LOGGER.warning( + "Waiting on integrations to complete setup: %s", + ", ".join(setup_started), + ) + loop_count = 0 + _LOGGER.debug("Running timeout Zones: %s", hass.timeout.zones) + + +async def async_setup_multi_components( + hass: core.HomeAssistant, + domains: set[str], + config: dict[str, Any], +) -> None: + """Set up multiple domains. Log on failure.""" + futures = { + domain: hass.async_create_task(async_setup_component(hass, domain, config)) + for domain in domains + } + await asyncio.wait(futures.values()) + errors = [domain for domain in domains if futures[domain].exception()] + for domain in errors: + exception = futures[domain].exception() + assert exception is not None + _LOGGER.error( + "Error setting up integration %s - received exception", + domain, + exc_info=(type(exception), exception, exception.__traceback__), + ) + + +async def _async_set_up_integrations( + hass: core.HomeAssistant, config: dict[str, Any] +) -> None: + """Set up all the integrations.""" + hass.data[DATA_SETUP_STARTED] = {} + setup_time = hass.data[DATA_SETUP_TIME] = {} + + watch_task = asyncio.create_task(_async_watch_pending_setups(hass)) + + domains_to_setup = _get_domains(hass, config) + + # Resolve all dependencies so we know all integrations + # that will have to be loaded and start rightaway + integration_cache: dict[str, loader.Integration] = {} + to_resolve = domains_to_setup + while to_resolve: + old_to_resolve = to_resolve + to_resolve = set() + + integrations_to_process = [ + int_or_exc + for int_or_exc in await gather_with_concurrency( + loader.MAX_LOAD_CONCURRENTLY, + *( + loader.async_get_integration(hass, domain) + for domain in old_to_resolve + ), + return_exceptions=True, + ) + if isinstance(int_or_exc, loader.Integration) + ] + resolve_dependencies_tasks = [ + itg.resolve_dependencies() + for itg in integrations_to_process + if not itg.all_dependencies_resolved + ] + + if resolve_dependencies_tasks: + await asyncio.gather(*resolve_dependencies_tasks) + + for itg in integrations_to_process: + integration_cache[itg.domain] = itg + + for dep in itg.all_dependencies: + if dep in domains_to_setup: + continue + + domains_to_setup.add(dep) + to_resolve.add(dep) + + _LOGGER.info("Domains to be set up: %s", domains_to_setup) + + logging_domains = domains_to_setup & LOGGING_INTEGRATIONS + + # Load logging as soon as possible + if logging_domains: + _LOGGER.info("Setting up logging: %s", logging_domains) + await async_setup_multi_components(hass, logging_domains, config) + + # Start up debuggers. Start these first in case they want to wait. + debuggers = domains_to_setup & DEBUGGER_INTEGRATIONS + + if debuggers: + _LOGGER.debug("Setting up debuggers: %s", debuggers) + await async_setup_multi_components(hass, debuggers, config) + + # calculate what components to setup in what stage + stage_1_domains = set() + + # Find all dependencies of any dependency of any stage 1 integration that + # we plan on loading and promote them to stage 1 + deps_promotion = STAGE_1_INTEGRATIONS + while deps_promotion: + old_deps_promotion = deps_promotion + deps_promotion = set() + + for domain in old_deps_promotion: + if domain not in domains_to_setup or domain in stage_1_domains: + continue + + stage_1_domains.add(domain) + + dep_itg = integration_cache.get(domain) + + if dep_itg is None: + continue + + deps_promotion.update(dep_itg.all_dependencies) + + stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains + + # Load the registries + await asyncio.gather( + device_registry.async_load(hass), + entity_registry.async_load(hass), + area_registry.async_load(hass), + ) + + # Start setup + if stage_1_domains: + _LOGGER.info("Setting up stage 1: %s", stage_1_domains) + try: + async with hass.timeout.async_timeout( + STAGE_1_TIMEOUT, cool_down=COOLDOWN_TIME + ): + await async_setup_multi_components(hass, stage_1_domains, config) + except asyncio.TimeoutError: + _LOGGER.warning("Setup timed out for stage 1 - moving forward") + + # Enables after dependencies + async_set_domains_to_be_loaded(hass, stage_2_domains) + + if stage_2_domains: + _LOGGER.info("Setting up stage 2: %s", stage_2_domains) + try: + async with hass.timeout.async_timeout( + STAGE_2_TIMEOUT, cool_down=COOLDOWN_TIME + ): + await async_setup_multi_components(hass, stage_2_domains, config) + except asyncio.TimeoutError: + _LOGGER.warning("Setup timed out for stage 2 - moving forward") + + watch_task.cancel() + async_dispatcher_send(hass, SIGNAL_BOOTSTRAP_INTEGRATONS, {}) + + _LOGGER.debug( + "Integration setup times: %s", + { + integration: timedelta.total_seconds() + for integration, timedelta in sorted( + setup_time.items(), key=lambda item: item[1].total_seconds() # type: ignore + ) + }, + ) + + # Wrap up startup + _LOGGER.debug("Waiting for startup to wrap up") + try: + async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME): + await hass.async_block_till_done() + except asyncio.TimeoutError: + _LOGGER.warning("Setup timed out for bootstrap - moving forward") diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/__init__.py new file mode 100644 index 00000000000..2a062109eaf --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/__init__.py @@ -0,0 +1,46 @@ +""" +This package contains components that can be plugged into Home Assistant. + +Component design guidelines: +- Each component defines a constant DOMAIN that is equal to its filename. +- Each component that tracks states should create state entity names in the + format ".". +- Each component should publish services only under its own domain. +""" +from __future__ import annotations + +import logging + +from homeassistant.core import HomeAssistant, split_entity_id + +_LOGGER = logging.getLogger(__name__) + + +def is_on(hass: HomeAssistant, entity_id: str | None = None) -> bool: + """Load up the module to call the is_on method. + + If there is no entity id given we will check all. + """ + if entity_id: + entity_ids = hass.components.group.expand_entity_ids([entity_id]) + else: + entity_ids = hass.states.entity_ids() + + for ent_id in entity_ids: + domain = split_entity_id(ent_id)[0] + + try: + component = getattr(hass.components, domain) + + except ImportError: + _LOGGER.error("Failed to call %s.is_on: component not found", domain) + continue + + if not hasattr(component, "is_on"): + _LOGGER.warning("Integration %s has no is_on method", domain) + continue + + if component.is_on(ent_id): + return True + + return False diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/__init__.py new file mode 100644 index 00000000000..156dbae2804 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/__init__.py @@ -0,0 +1,409 @@ +"""Support for the Abode Security System.""" +from copy import deepcopy +from functools import partial + +from abodepy import Abode +from abodepy.exceptions import AbodeAuthenticationException, AbodeException +import abodepy.helpers.timeline as TIMELINE +from requests.exceptions import ConnectTimeout, HTTPError +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_DATE, + ATTR_DEVICE_ID, + ATTR_ENTITY_ID, + ATTR_TIME, + CONF_PASSWORD, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.entity import Entity + +from .const import ATTRIBUTION, DEFAULT_CACHEDB, DOMAIN, LOGGER + +CONF_POLLING = "polling" + +SERVICE_SETTINGS = "change_setting" +SERVICE_CAPTURE_IMAGE = "capture_image" +SERVICE_TRIGGER_AUTOMATION = "trigger_automation" + +ATTR_DEVICE_NAME = "device_name" +ATTR_DEVICE_TYPE = "device_type" +ATTR_EVENT_CODE = "event_code" +ATTR_EVENT_NAME = "event_name" +ATTR_EVENT_TYPE = "event_type" +ATTR_EVENT_UTC = "event_utc" +ATTR_SETTING = "setting" +ATTR_USER_NAME = "user_name" +ATTR_APP_TYPE = "app_type" +ATTR_EVENT_BY = "event_by" +ATTR_VALUE = "value" + +CONFIG_SCHEMA = vol.Schema( + vol.All( + # Deprecated in Home Assistant 2021.6 + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_POLLING, default=False): cv.boolean, + } + ) + }, + ), + extra=vol.ALLOW_EXTRA, +) + +CHANGE_SETTING_SCHEMA = vol.Schema( + {vol.Required(ATTR_SETTING): cv.string, vol.Required(ATTR_VALUE): cv.string} +) + +CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids}) + +AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids}) + +PLATFORMS = [ + "alarm_control_panel", + "binary_sensor", + "lock", + "switch", + "cover", + "camera", + "light", + "sensor", +] + + +class AbodeSystem: + """Abode System class.""" + + def __init__(self, abode, polling): + """Initialize the system.""" + self.abode = abode + self.polling = polling + self.entity_ids = set() + self.logout_listener = None + + +async def async_setup(hass, config): + """Set up Abode integration.""" + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=deepcopy(conf) + ) + ) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up Abode integration from a config entry.""" + username = config_entry.data.get(CONF_USERNAME) + password = config_entry.data.get(CONF_PASSWORD) + polling = config_entry.data.get(CONF_POLLING) + cache = hass.config.path(DEFAULT_CACHEDB) + + # For previous config entries where unique_id is None + if config_entry.unique_id is None: + hass.config_entries.async_update_entry( + config_entry, unique_id=config_entry.data[CONF_USERNAME] + ) + + try: + abode = await hass.async_add_executor_job( + Abode, username, password, True, True, True, cache + ) + + except AbodeAuthenticationException as ex: + raise ConfigEntryAuthFailed(f"Invalid credentials: {ex}") from ex + + except (AbodeException, ConnectTimeout, HTTPError) as ex: + raise ConfigEntryNotReady(f"Unable to connect to Abode: {ex}") from ex + + hass.data[DOMAIN] = AbodeSystem(abode, polling) + + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + + await setup_hass_events(hass) + await hass.async_add_executor_job(setup_hass_services, hass) + await hass.async_add_executor_job(setup_abode_events, hass) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + hass.services.async_remove(DOMAIN, SERVICE_SETTINGS) + hass.services.async_remove(DOMAIN, SERVICE_CAPTURE_IMAGE) + hass.services.async_remove(DOMAIN, SERVICE_TRIGGER_AUTOMATION) + + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + + await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.stop) + await hass.async_add_executor_job(hass.data[DOMAIN].abode.logout) + + hass.data[DOMAIN].logout_listener() + hass.data.pop(DOMAIN) + + return unload_ok + + +def setup_hass_services(hass): + """Home Assistant services.""" + + def change_setting(call): + """Change an Abode system setting.""" + setting = call.data.get(ATTR_SETTING) + value = call.data.get(ATTR_VALUE) + + try: + hass.data[DOMAIN].abode.set_setting(setting, value) + except AbodeException as ex: + LOGGER.warning(ex) + + def capture_image(call): + """Capture a new image.""" + entity_ids = call.data.get(ATTR_ENTITY_ID) + + target_entities = [ + entity_id + for entity_id in hass.data[DOMAIN].entity_ids + if entity_id in entity_ids + ] + + for entity_id in target_entities: + signal = f"abode_camera_capture_{entity_id}" + dispatcher_send(hass, signal) + + def trigger_automation(call): + """Trigger an Abode automation.""" + entity_ids = call.data.get(ATTR_ENTITY_ID) + + target_entities = [ + entity_id + for entity_id in hass.data[DOMAIN].entity_ids + if entity_id in entity_ids + ] + + for entity_id in target_entities: + signal = f"abode_trigger_automation_{entity_id}" + dispatcher_send(hass, signal) + + hass.services.register( + DOMAIN, SERVICE_SETTINGS, change_setting, schema=CHANGE_SETTING_SCHEMA + ) + + hass.services.register( + DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image, schema=CAPTURE_IMAGE_SCHEMA + ) + + hass.services.register( + DOMAIN, SERVICE_TRIGGER_AUTOMATION, trigger_automation, schema=AUTOMATION_SCHEMA + ) + + +async def setup_hass_events(hass): + """Home Assistant start and stop callbacks.""" + + def logout(event): + """Logout of Abode.""" + if not hass.data[DOMAIN].polling: + hass.data[DOMAIN].abode.events.stop() + + hass.data[DOMAIN].abode.logout() + LOGGER.info("Logged out of Abode") + + if not hass.data[DOMAIN].polling: + await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.start) + + hass.data[DOMAIN].logout_listener = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, logout + ) + + +def setup_abode_events(hass): + """Event callbacks.""" + + def event_callback(event, event_json): + """Handle an event callback from Abode.""" + data = { + ATTR_DEVICE_ID: event_json.get(ATTR_DEVICE_ID, ""), + ATTR_DEVICE_NAME: event_json.get(ATTR_DEVICE_NAME, ""), + ATTR_DEVICE_TYPE: event_json.get(ATTR_DEVICE_TYPE, ""), + ATTR_EVENT_CODE: event_json.get(ATTR_EVENT_CODE, ""), + ATTR_EVENT_NAME: event_json.get(ATTR_EVENT_NAME, ""), + ATTR_EVENT_TYPE: event_json.get(ATTR_EVENT_TYPE, ""), + ATTR_EVENT_UTC: event_json.get(ATTR_EVENT_UTC, ""), + ATTR_USER_NAME: event_json.get(ATTR_USER_NAME, ""), + ATTR_APP_TYPE: event_json.get(ATTR_APP_TYPE, ""), + ATTR_EVENT_BY: event_json.get(ATTR_EVENT_BY, ""), + ATTR_DATE: event_json.get(ATTR_DATE, ""), + ATTR_TIME: event_json.get(ATTR_TIME, ""), + } + + hass.bus.fire(event, data) + + events = [ + TIMELINE.ALARM_GROUP, + TIMELINE.ALARM_END_GROUP, + TIMELINE.PANEL_FAULT_GROUP, + TIMELINE.PANEL_RESTORE_GROUP, + TIMELINE.AUTOMATION_GROUP, + TIMELINE.DISARM_GROUP, + TIMELINE.ARM_GROUP, + TIMELINE.ARM_FAULT_GROUP, + TIMELINE.TEST_GROUP, + TIMELINE.CAPTURE_GROUP, + TIMELINE.DEVICE_GROUP, + ] + + for event in events: + hass.data[DOMAIN].abode.events.add_event_callback( + event, partial(event_callback, event) + ) + + +class AbodeEntity(Entity): + """Representation of an Abode entity.""" + + def __init__(self, data): + """Initialize Abode entity.""" + self._data = data + self._available = True + + @property + def available(self): + """Return the available state.""" + return self._available + + @property + def should_poll(self): + """Return the polling state.""" + return self._data.polling + + async def async_added_to_hass(self): + """Subscribe to Abode connection status updates.""" + await self.hass.async_add_executor_job( + self._data.abode.events.add_connection_status_callback, + self.unique_id, + self._update_connection_status, + ) + + self.hass.data[DOMAIN].entity_ids.add(self.entity_id) + + async def async_will_remove_from_hass(self): + """Unsubscribe from Abode connection status updates.""" + await self.hass.async_add_executor_job( + self._data.abode.events.remove_connection_status_callback, self.unique_id + ) + + def _update_connection_status(self): + """Update the entity available property.""" + self._available = self._data.abode.events.connected + self.schedule_update_ha_state() + + +class AbodeDevice(AbodeEntity): + """Representation of an Abode device.""" + + def __init__(self, data, device): + """Initialize Abode device.""" + super().__init__(data) + self._device = device + + async def async_added_to_hass(self): + """Subscribe to device events.""" + await super().async_added_to_hass() + await self.hass.async_add_executor_job( + self._data.abode.events.add_device_callback, + self._device.device_id, + self._update_callback, + ) + + async def async_will_remove_from_hass(self): + """Unsubscribe from device events.""" + await super().async_will_remove_from_hass() + await self.hass.async_add_executor_job( + self._data.abode.events.remove_all_device_callbacks, self._device.device_id + ) + + def update(self): + """Update device state.""" + self._device.refresh() + + @property + def name(self): + """Return the name of the device.""" + return self._device.name + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: ATTRIBUTION, + "device_id": self._device.device_id, + "battery_low": self._device.battery_low, + "no_response": self._device.no_response, + "device_type": self._device.type, + } + + @property + def unique_id(self): + """Return a unique ID to use for this device.""" + return self._device.device_uuid + + @property + def device_info(self): + """Return device registry information for this entity.""" + return { + "identifiers": {(DOMAIN, self._device.device_id)}, + "manufacturer": "Abode", + "name": self._device.name, + "device_type": self._device.type, + } + + def _update_callback(self, device): + """Update the device state.""" + self.schedule_update_ha_state() + + +class AbodeAutomation(AbodeEntity): + """Representation of an Abode automation.""" + + def __init__(self, data, automation): + """Initialize for Abode automation.""" + super().__init__(data) + self._automation = automation + + def update(self): + """Update automation state.""" + self._automation.refresh() + + @property + def name(self): + """Return the name of the automation.""" + return self._automation.name + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION, "type": "CUE automation"} + + @property + def unique_id(self): + """Return a unique ID to use for this automation.""" + return self._automation.automation_id diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/alarm_control_panel.py b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/alarm_control_panel.py new file mode 100644 index 00000000000..6d0c030e3e1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/alarm_control_panel.py @@ -0,0 +1,79 @@ +"""Support for Abode Security System alarm control panels.""" +import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) +from homeassistant.const import ( + ATTR_ATTRIBUTION, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, +) + +from . import AbodeDevice +from .const import ATTRIBUTION, DOMAIN + +ICON = "mdi:security" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Abode alarm control panel device.""" + data = hass.data[DOMAIN] + async_add_entities( + [AbodeAlarm(data, await hass.async_add_executor_job(data.abode.get_alarm))] + ) + + +class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity): + """An alarm_control_panel implementation for Abode.""" + + @property + def icon(self): + """Return the icon.""" + return ICON + + @property + def state(self): + """Return the state of the device.""" + if self._device.is_standby: + state = STATE_ALARM_DISARMED + elif self._device.is_away: + state = STATE_ALARM_ARMED_AWAY + elif self._device.is_home: + state = STATE_ALARM_ARMED_HOME + else: + state = None + return state + + @property + def code_arm_required(self): + """Whether the code is required for arm actions.""" + return False + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + + def alarm_disarm(self, code=None): + """Send disarm command.""" + self._device.set_standby() + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + self._device.set_home() + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + self._device.set_away() + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: ATTRIBUTION, + "device_id": self._device.device_id, + "battery_backup": self._device.battery, + "cellular_backup": self._device.is_cellular, + } diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/binary_sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/binary_sensor.py new file mode 100644 index 00000000000..7175fbc550a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/binary_sensor.py @@ -0,0 +1,46 @@ +"""Support for Abode Security System binary sensors.""" +import abodepy.helpers.constants as CONST + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_WINDOW, + BinarySensorEntity, +) + +from . import AbodeDevice +from .const import DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Abode binary sensor devices.""" + data = hass.data[DOMAIN] + + device_types = [ + CONST.TYPE_CONNECTIVITY, + CONST.TYPE_MOISTURE, + CONST.TYPE_MOTION, + CONST.TYPE_OCCUPANCY, + CONST.TYPE_OPENING, + ] + + entities = [] + + for device in data.abode.get_devices(generic_type=device_types): + entities.append(AbodeBinarySensor(data, device)) + + async_add_entities(entities) + + +class AbodeBinarySensor(AbodeDevice, BinarySensorEntity): + """A binary sensor implementation for Abode device.""" + + @property + def is_on(self): + """Return True if the binary sensor is on.""" + return self._device.is_on + + @property + def device_class(self): + """Return the class of the binary sensor.""" + if self._device.get_value("is_window") == "1": + return DEVICE_CLASS_WINDOW + return self._device.generic_type diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/camera.py b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/camera.py new file mode 100644 index 00000000000..99d4fd433a7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/camera.py @@ -0,0 +1,102 @@ +"""Support for Abode Security System cameras.""" +from datetime import timedelta + +import abodepy.helpers.constants as CONST +import abodepy.helpers.timeline as TIMELINE +import requests + +from homeassistant.components.camera import Camera +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util import Throttle + +from . import AbodeDevice +from .const import DOMAIN, LOGGER + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Abode camera devices.""" + data = hass.data[DOMAIN] + + entities = [] + + for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA): + entities.append(AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE)) + + async_add_entities(entities) + + +class AbodeCamera(AbodeDevice, Camera): + """Representation of an Abode camera.""" + + def __init__(self, data, device, event): + """Initialize the Abode device.""" + AbodeDevice.__init__(self, data, device) + Camera.__init__(self) + self._event = event + self._response = None + + async def async_added_to_hass(self): + """Subscribe Abode events.""" + await super().async_added_to_hass() + + self.hass.async_add_executor_job( + self._data.abode.events.add_timeline_callback, + self._event, + self._capture_callback, + ) + + signal = f"abode_camera_capture_{self.entity_id}" + self.async_on_remove(async_dispatcher_connect(self.hass, signal, self.capture)) + + def capture(self): + """Request a new image capture.""" + return self._device.capture() + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def refresh_image(self): + """Find a new image on the timeline.""" + if self._device.refresh_image(): + self.get_image() + + def get_image(self): + """Attempt to download the most recent capture.""" + if self._device.image_url: + try: + self._response = requests.get(self._device.image_url, stream=True) + + self._response.raise_for_status() + except requests.HTTPError as err: + LOGGER.warning("Failed to get camera image: %s", err) + self._response = None + else: + self._response = None + + def camera_image(self): + """Get a camera image.""" + self.refresh_image() + + if self._response: + return self._response.content + + return None + + def turn_on(self): + """Turn on camera.""" + self._device.privacy_mode(False) + + def turn_off(self): + """Turn off camera.""" + self._device.privacy_mode(True) + + def _capture_callback(self, capture): + """Update the image with the device then refresh device.""" + self._device.update_image_location(capture) + self.get_image() + self.schedule_update_ha_state() + + @property + def is_on(self): + """Return true if on.""" + return self._device.is_on diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/config_flow.py new file mode 100644 index 00000000000..bf51ffee81c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/config_flow.py @@ -0,0 +1,170 @@ +"""Config flow for the Abode Security System component.""" +from abodepy import Abode +from abodepy.exceptions import AbodeAuthenticationException, AbodeException +from abodepy.helpers.errors import MFA_CODE_REQUIRED +from requests.exceptions import ConnectTimeout, HTTPError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, HTTP_BAD_REQUEST + +from .const import DEFAULT_CACHEDB, DOMAIN, LOGGER + +CONF_MFA = "mfa_code" +CONF_POLLING = "polling" + + +class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for Abode.""" + + VERSION = 1 + + def __init__(self): + """Initialize.""" + self.data_schema = { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + self.mfa_data_schema = { + vol.Required(CONF_MFA): str, + } + + self._cache = None + self._mfa_code = None + self._password = None + self._polling = False + self._username = None + + async def _async_abode_login(self, step_id): + """Handle login with Abode.""" + self._cache = self.hass.config.path(DEFAULT_CACHEDB) + errors = {} + + try: + await self.hass.async_add_executor_job( + Abode, self._username, self._password, True, False, False, self._cache + ) + + except (AbodeException, ConnectTimeout, HTTPError) as ex: + if ex.errcode == MFA_CODE_REQUIRED[0]: + return await self.async_step_mfa() + + LOGGER.error("Unable to connect to Abode: %s", ex) + + if ex.errcode == HTTP_BAD_REQUEST: + errors = {"base": "invalid_auth"} + + else: + errors = {"base": "cannot_connect"} + + if errors: + return self.async_show_form( + step_id=step_id, data_schema=vol.Schema(self.data_schema), errors=errors + ) + + return await self._async_create_entry() + + async def _async_abode_mfa_login(self): + """Handle multi-factor authentication (MFA) login with Abode.""" + try: + # Create instance to access login method for passing MFA code + abode = Abode( + auto_login=False, + get_devices=False, + get_automations=False, + cache_path=self._cache, + ) + await self.hass.async_add_executor_job( + abode.login, self._username, self._password, self._mfa_code + ) + + except AbodeAuthenticationException: + return self.async_show_form( + step_id="mfa", + data_schema=vol.Schema(self.mfa_data_schema), + errors={"base": "invalid_mfa_code"}, + ) + + return await self._async_create_entry() + + async def _async_create_entry(self): + """Create the config entry.""" + config_data = { + CONF_USERNAME: self._username, + CONF_PASSWORD: self._password, + CONF_POLLING: self._polling, + } + existing_entry = await self.async_set_unique_id(self._username) + + if existing_entry: + self.hass.config_entries.async_update_entry( + existing_entry, data=config_data + ) + # Reload the Abode config entry otherwise devices will remain unavailable + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) + + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry(title=self._username, data=config_data) + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=vol.Schema(self.data_schema) + ) + + self._username = user_input[CONF_USERNAME] + self._password = user_input[CONF_PASSWORD] + + return await self._async_abode_login(step_id="user") + + async def async_step_mfa(self, user_input=None): + """Handle a multi-factor authentication (MFA) flow.""" + if user_input is None: + return self.async_show_form( + step_id="mfa", data_schema=vol.Schema(self.mfa_data_schema) + ) + + self._mfa_code = user_input[CONF_MFA] + + return await self._async_abode_mfa_login() + + async def async_step_reauth(self, config): + """Handle reauthorization request from Abode.""" + self._username = config[CONF_USERNAME] + + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Handle reauthorization flow.""" + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_USERNAME, default=self._username): str, + vol.Required(CONF_PASSWORD): str, + } + ), + ) + + self._username = user_input[CONF_USERNAME] + self._password = user_input[CONF_PASSWORD] + + return await self._async_abode_login(step_id="reauth_confirm") + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + if self._async_current_entries(): + LOGGER.warning("Already configured; Only a single configuration possible") + return self.async_abort(reason="single_instance_allowed") + + self._polling = import_config.get(CONF_POLLING, False) + + return await self.async_step_user(import_config) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/const.py new file mode 100644 index 00000000000..b509984876b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/const.py @@ -0,0 +1,9 @@ +"""Constants for the Abode Security System component.""" +import logging + +LOGGER = logging.getLogger(__package__) + +DOMAIN = "abode" +ATTRIBUTION = "Data provided by goabode.com" + +DEFAULT_CACHEDB = "abodepy_cache.pickle" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/cover.py b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/cover.py new file mode 100644 index 00000000000..d88c2fdd404 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/cover.py @@ -0,0 +1,36 @@ +"""Support for Abode Security System covers.""" +import abodepy.helpers.constants as CONST + +from homeassistant.components.cover import CoverEntity + +from . import AbodeDevice +from .const import DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Abode cover devices.""" + data = hass.data[DOMAIN] + + entities = [] + + for device in data.abode.get_devices(generic_type=CONST.TYPE_COVER): + entities.append(AbodeCover(data, device)) + + async_add_entities(entities) + + +class AbodeCover(AbodeDevice, CoverEntity): + """Representation of an Abode cover.""" + + @property + def is_closed(self): + """Return true if cover is closed, else False.""" + return not self._device.is_open + + def close_cover(self, **kwargs): + """Issue close command to cover.""" + self._device.close_cover() + + def open_cover(self, **kwargs): + """Issue open command to cover.""" + self._device.open_cover() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/light.py b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/light.py new file mode 100644 index 00000000000..b756c79d9de --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/light.py @@ -0,0 +1,98 @@ +"""Support for Abode Security System lights.""" +from math import ceil + +import abodepy.helpers.constants as CONST + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + LightEntity, +) +from homeassistant.util.color import ( + color_temperature_kelvin_to_mired, + color_temperature_mired_to_kelvin, +) + +from . import AbodeDevice +from .const import DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Abode light devices.""" + data = hass.data[DOMAIN] + + entities = [] + + for device in data.abode.get_devices(generic_type=CONST.TYPE_LIGHT): + entities.append(AbodeLight(data, device)) + + async_add_entities(entities) + + +class AbodeLight(AbodeDevice, LightEntity): + """Representation of an Abode light.""" + + def turn_on(self, **kwargs): + """Turn on the light.""" + if ATTR_COLOR_TEMP in kwargs and self._device.is_color_capable: + self._device.set_color_temp( + int(color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP])) + ) + return + + if ATTR_HS_COLOR in kwargs and self._device.is_color_capable: + self._device.set_color(kwargs[ATTR_HS_COLOR]) + return + + if ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable: + # Convert Home Assistant brightness (0-255) to Abode brightness (0-99) + # If 100 is sent to Abode, response is 99 causing an error + self._device.set_level(ceil(kwargs[ATTR_BRIGHTNESS] * 99 / 255.0)) + return + + self._device.switch_on() + + def turn_off(self, **kwargs): + """Turn off the light.""" + self._device.switch_off() + + @property + def is_on(self): + """Return true if device is on.""" + return self._device.is_on + + @property + def brightness(self): + """Return the brightness of the light.""" + if self._device.is_dimmable and self._device.has_brightness: + brightness = int(self._device.brightness) + # Abode returns 100 during device initialization and device refresh + if brightness == 100: + return 255 + # Convert Abode brightness (0-99) to Home Assistant brightness (0-255) + return ceil(brightness * 255 / 99.0) + + @property + def color_temp(self): + """Return the color temp of the light.""" + if self._device.has_color: + return color_temperature_kelvin_to_mired(self._device.color_temp) + + @property + def hs_color(self): + """Return the color of the light.""" + if self._device.has_color: + return self._device.color + + @property + def supported_features(self): + """Flag supported features.""" + if self._device.is_dimmable and self._device.is_color_capable: + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_COLOR_TEMP + if self._device.is_dimmable: + return SUPPORT_BRIGHTNESS + return 0 diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/lock.py b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/lock.py new file mode 100644 index 00000000000..2a52663c0e7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/lock.py @@ -0,0 +1,36 @@ +"""Support for the Abode Security System locks.""" +import abodepy.helpers.constants as CONST + +from homeassistant.components.lock import LockEntity + +from . import AbodeDevice +from .const import DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Abode lock devices.""" + data = hass.data[DOMAIN] + + entities = [] + + for device in data.abode.get_devices(generic_type=CONST.TYPE_LOCK): + entities.append(AbodeLock(data, device)) + + async_add_entities(entities) + + +class AbodeLock(AbodeDevice, LockEntity): + """Representation of an Abode lock.""" + + def lock(self, **kwargs): + """Lock the device.""" + self._device.lock() + + def unlock(self, **kwargs): + """Unlock the device.""" + self._device.unlock() + + @property + def is_locked(self): + """Return true if device is on.""" + return self._device.is_locked diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/manifest.json new file mode 100644 index 00000000000..c9353c31bab --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "abode", + "name": "Abode", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/abode", + "requirements": ["abodepy==1.2.0"], + "codeowners": ["@shred86"], + "homekit": { + "models": ["Abode", "Iota"] + }, + "iot_class": "cloud_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/sensor.py new file mode 100644 index 00000000000..e3ececb62d9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/sensor.py @@ -0,0 +1,80 @@ +"""Support for Abode Security System sensors.""" +import abodepy.helpers.constants as CONST + +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_TEMPERATURE, +) + +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], +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Abode sensor devices.""" + data = hass.data[DOMAIN] + + 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)) + + async_add_entities(entities) + + +class AbodeSensor(AbodeDevice, SensorEntity): + """A sensor implementation for Abode devices.""" + + def __init__(self, data, device, sensor_type): + """Initialize a sensor for an Abode device.""" + super().__init__(data, device) + self._sensor_type = sensor_type + self._name = f"{self._device.name} {SENSOR_TYPES[self._sensor_type][0]}" + self._device_class = SENSOR_TYPES[self._sensor_type][1] + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @property + def unique_id(self): + """Return a unique ID to use for this device.""" + return f"{self._device.device_uuid}-{self._sensor_type}" + + @property + def state(self): + """Return the state of the sensor.""" + if self._sensor_type == CONST.TEMP_STATUS_KEY: + return self._device.temp + if self._sensor_type == CONST.HUMI_STATUS_KEY: + return self._device.humidity + if self._sensor_type == CONST.LUX_STATUS_KEY: + return self._device.lux + + @property + def unit_of_measurement(self): + """Return the units of measurement.""" + if self._sensor_type == CONST.TEMP_STATUS_KEY: + return self._device.temp_unit + if self._sensor_type == CONST.HUMI_STATUS_KEY: + return self._device.humidity_unit + if self._sensor_type == CONST.LUX_STATUS_KEY: + return self._device.lux_unit diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/services.yaml b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/services.yaml new file mode 100644 index 00000000000..9b5362c0929 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/services.yaml @@ -0,0 +1,46 @@ +capture_image: + name: Capture image + description: Request a new image capture from a camera device. + fields: + entity_id: + name: Entity + description: Entity id of the camera to request an image. + required: true + example: camera.downstairs_motion_camera + selector: + entity: + integration: abode + domain: camera + +change_setting: + name: Change setting + description: Change an Abode system setting. + fields: + setting: + name: Setting + description: Setting to change. + required: true + example: beeper_mute + selector: + text: + value: + name: Value + description: Value of the setting. + required: true + example: "1" + selector: + text: + +trigger_automation: + name: Trigger automation + description: Trigger an Abode automation. + fields: + entity_id: + name: Entity + description: Entity id of the automation to trigger. + required: true + example: switch.my_automation + selector: + entity: + integration: abode + domain: switch diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/strings.json new file mode 100644 index 00000000000..14a60f827c3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/strings.json @@ -0,0 +1,36 @@ +{ + "config": { + "step": { + "user": { + "title": "Fill in your Abode login information", + "data": { + "username": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, + "mfa": { + "title": "Enter your MFA code for Abode", + "data": { + "mfa_code": "MFA code (6-digits)" + } + }, + "reauth_confirm": { + "title": "Fill in your Abode login information", + "data": { + "username": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_mfa_code": "Invalid MFA code" + + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/switch.py b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/switch.py new file mode 100644 index 00000000000..0985ce5ce2a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/switch.py @@ -0,0 +1,80 @@ +"""Support for Abode Security System switches.""" +import abodepy.helpers.constants as CONST + +from homeassistant.components.switch import SwitchEntity +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import AbodeAutomation, AbodeDevice +from .const import DOMAIN + +DEVICE_TYPES = [CONST.TYPE_SWITCH, CONST.TYPE_VALVE] + +ICON = "mdi:robot" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Abode switch devices.""" + data = hass.data[DOMAIN] + + entities = [] + + for device_type in DEVICE_TYPES: + for device in data.abode.get_devices(generic_type=device_type): + entities.append(AbodeSwitch(data, device)) + + for automation in data.abode.get_automations(): + entities.append(AbodeAutomationSwitch(data, automation)) + + async_add_entities(entities) + + +class AbodeSwitch(AbodeDevice, SwitchEntity): + """Representation of an Abode switch.""" + + def turn_on(self, **kwargs): + """Turn on the device.""" + self._device.switch_on() + + def turn_off(self, **kwargs): + """Turn off the device.""" + self._device.switch_off() + + @property + def is_on(self): + """Return true if device is on.""" + return self._device.is_on + + +class AbodeAutomationSwitch(AbodeAutomation, SwitchEntity): + """A switch implementation for Abode automations.""" + + async def async_added_to_hass(self): + """Set up trigger automation service.""" + await super().async_added_to_hass() + + signal = f"abode_trigger_automation_{self.entity_id}" + self.async_on_remove(async_dispatcher_connect(self.hass, signal, self.trigger)) + + def turn_on(self, **kwargs): + """Enable the automation.""" + if self._automation.enable(True): + self.schedule_update_ha_state() + + def turn_off(self, **kwargs): + """Disable the automation.""" + if self._automation.enable(False): + self.schedule_update_ha_state() + + def trigger(self): + """Trigger the automation.""" + self._automation.trigger() + + @property + def is_on(self): + """Return True if the automation is enabled.""" + return self._automation.is_enabled + + @property + def icon(self): + """Return the robot icon to match Home Assistant automations.""" + return ICON diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/bg.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/bg.json new file mode 100644 index 00000000000..285bf18d330 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/bg.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 Abode." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "E-mail \u0430\u0434\u0440\u0435\u0441" + }, + "title": "\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0412\u0430\u0448\u0430\u0442\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0437\u0430 \u0432\u0445\u043e\u0434 \u0432 Abode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/ca.json new file mode 100644 index 00000000000..1d758bc4398 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/ca.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_mfa_code": "Codi MFA inv\u00e0lid" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "Codi MFA (6 d\u00edgits)" + }, + "title": "Introdueix el codi MFA per a Abode" + }, + "reauth_confirm": { + "data": { + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + }, + "title": "Introdueix la informaci\u00f3 d'inici de sessi\u00f3 d'Abode." + }, + "user": { + "data": { + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + }, + "title": "Introducci\u00f3 de la informaci\u00f3 d'inici de sessi\u00f3 a Abode." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/cs.json new file mode 100644 index 00000000000..30ffaa74a32 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/cs.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "invalid_mfa_code": "Neplatn\u00fd k\u00f3d MFA" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "K\u00f3d MFA (6 \u010d\u00edslic)" + }, + "title": "Zadejte k\u00f3d MFA pro Abode" + }, + "reauth_confirm": { + "data": { + "password": "Heslo", + "username": "E-mail" + }, + "title": "Vypl\u0148te sv\u00e9 p\u0159ihla\u0161ovac\u00ed \u00fadaje do Abode" + }, + "user": { + "data": { + "password": "Heslo", + "username": "E-mail" + }, + "title": "Vypl\u0148te p\u0159ihla\u0161ovac\u00ed \u00fadaje Abode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/da.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/da.json new file mode 100644 index 00000000000..a6f8d3ddd66 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/da.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af Abode." + }, + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Email-adresse" + }, + "title": "Udfyld dine Abode-loginoplysninger" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/de.json new file mode 100644 index 00000000000..307f5f45065 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/de.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_mfa_code": "Ung\u00fcltiger MFA-Code" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "MFA-Code (6-stellig)" + }, + "title": "Gib deinen MFA-Code f\u00fcr Abode ein" + }, + "reauth_confirm": { + "data": { + "password": "Passwort", + "username": "E-Mail" + }, + "title": "Gib deine Abode-Anmeldeinformationen ein" + }, + "user": { + "data": { + "password": "Passwort", + "username": "E-Mail-Adresse" + }, + "title": "Gib deine Abode-Anmeldeinformationen ein" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/el.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/el.json new file mode 100644 index 00000000000..b30be708065 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/el.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c5\u03b8\u03b5\u03bd\u03c4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/en.json new file mode 100644 index 00000000000..c1deaf0a00c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/en.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "reauth_successful": "Re-authentication was successful", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_mfa_code": "Invalid MFA code" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "MFA code (6-digits)" + }, + "title": "Enter your MFA code for Abode" + }, + "reauth_confirm": { + "data": { + "password": "Password", + "username": "Email" + }, + "title": "Fill in your Abode login information" + }, + "user": { + "data": { + "password": "Password", + "username": "Email" + }, + "title": "Fill in your Abode login information" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/es-419.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/es-419.json new file mode 100644 index 00000000000..9de6d9d185a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/es-419.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de Abode." + }, + "error": { + "invalid_mfa_code": "C\u00f3digo MFA no v\u00e1lido" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "C\u00f3digo MFA (6 d\u00edgitos)" + }, + "title": "Ingrese su c\u00f3digo MFA para Abode" + }, + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, + "title": "Complete su informaci\u00f3n de inicio de sesi\u00f3n de Abode" + }, + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Direcci\u00f3n de correo electr\u00f3nico" + }, + "title": "Complete su informaci\u00f3n de inicio de sesi\u00f3n de Abode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/es.json new file mode 100644 index 00000000000..66cb5d13f22 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/es.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_mfa_code": "C\u00f3digo MFA inv\u00e1lido" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "C\u00f3digo MFA (6 d\u00edgitos)" + }, + "title": "Introduce tu c\u00f3digo MFA para Abode" + }, + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a", + "username": "Correo electr\u00f3nico" + }, + "title": "Rellene su informaci\u00f3n de inicio de sesi\u00f3n de Abode" + }, + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Correo electr\u00f3nico" + }, + "title": "Rellene la informaci\u00f3n de acceso Abode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/et.json new file mode 100644 index 00000000000..f44b4ae25c4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/et.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamise viga", + "invalid_mfa_code": "Kehtetu MFA-kood" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "MFA kood (6-kohaline)" + }, + "title": "Sisesta oma Abode MFA kood" + }, + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na", + "username": "E-post" + }, + "title": "Sisesta oma Abode sisselogimisteave" + }, + "user": { + "data": { + "password": "Salas\u00f5na", + "username": "E-post" + }, + "title": "Sisesta oma Abode sisselogimisteave" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/fa.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/fa.json new file mode 100644 index 00000000000..4ceaaf32a13 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/fa.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u06a9\u0644\u0645\u0647 \u0639\u0628\u0648\u0631", + "username": "\u0627\u06cc\u0645\u06cc\u0644" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/fr.json new file mode 100644 index 00000000000..2ab158cca57 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/fr.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "single_instance_allowed": "D\u00e9ja configur\u00e9. Une seule configuration possible." + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "invalid_mfa_code": "Code MFA non valide" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "Code MFA (6 chiffres)" + }, + "title": "Entrez votre code MFA pour Abode" + }, + "reauth_confirm": { + "data": { + "password": "Mot de passe", + "username": "Email" + }, + "title": "Remplissez vos informations de connexion Abode" + }, + "user": { + "data": { + "password": "Mot de passe", + "username": "Email" + }, + "title": "Remplissez vos informations de connexion Abode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/he.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/he.json new file mode 100644 index 00000000000..6f4191da70d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/hu.json new file mode 100644 index 00000000000..260416b07bb --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/hu.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_mfa_code": "\u00c9rv\u00e9nytelen MFA k\u00f3d" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "MFA k\u00f3d (6 jegy\u0171)" + }, + "title": "Add meg az Abode MFA k\u00f3dj\u00e1t" + }, + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3", + "username": "E-mail" + } + }, + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "E-mail" + }, + "title": "T\u00f6ltse ki az Abode bejelentkez\u00e9si adatait" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/id.json new file mode 100644 index 00000000000..2dc79c833b2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/id.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "reauth_successful": "Autentikasi ulang berhasil", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "invalid_mfa_code": "Kode MFA tidak valid" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "Kode MFA (6 digit)" + }, + "title": "Masukkan kode MFA Anda untuk Abode" + }, + "reauth_confirm": { + "data": { + "password": "Kata Sandi", + "username": "Email" + }, + "title": "Masukkan informasi masuk Abode Anda" + }, + "user": { + "data": { + "password": "Kata Sandi", + "username": "Email" + }, + "title": "Masukkan informasi masuk Abode Anda" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/it.json new file mode 100644 index 00000000000..6cb571df8e5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/it.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "invalid_mfa_code": "Codice MFA non valido" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "Codice MFA (6 cifre)" + }, + "title": "Inserisci il tuo codice MFA per Abode" + }, + "reauth_confirm": { + "data": { + "password": "Password", + "username": "E-mail" + }, + "title": "Inserisci le tue informazioni di accesso Abode" + }, + "user": { + "data": { + "password": "Password", + "username": "E-mail" + }, + "title": "Inserisci le tue informazioni di accesso Abode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/ko.json new file mode 100644 index 00000000000..85d3ef81aeb --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/ko.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_mfa_code": "MFA \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "MFA \ucf54\ub4dc (6\uc790\ub9ac)" + }, + "title": "Abode\uc5d0 \ub300\ud55c MFA \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" + }, + "reauth_confirm": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c" + }, + "title": "Abode \ub85c\uadf8\uc778 \uc815\ubcf4 \uc785\ub825\ud558\uae30" + }, + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c" + }, + "title": "Abode \uc0ac\uc6a9\uc790 \uc815\ubcf4 \uc785\ub825\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/lb.json new file mode 100644 index 00000000000..2058d3353c9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/lb.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "reauth_successful": "Re-authentifikatioun war erfollegr\u00e4ich", + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "invalid_mfa_code": "Ong\u00ebltege MFA Code" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "MFA code (6 Zifferen)" + }, + "title": "G\u00ebff dain MFA code fir Abode un" + }, + "reauth_confirm": { + "data": { + "password": "Passwuert", + "username": "E-Mail" + }, + "title": "F\u00ebll deng Abode Login Informatiounen aus" + }, + "user": { + "data": { + "password": "Passwuert", + "username": "E-Mail" + }, + "title": "F\u00ebllt \u00e4r Abode Login Informatiounen aus." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/lv.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/lv.json new file mode 100644 index 00000000000..eab98211e14 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/lv.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Parole", + "username": "E-pasta adrese" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/nl.json new file mode 100644 index 00000000000..7b6a8b5aace --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/nl.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "reauth_successful": "Herauthenticatie was succesvol", + "single_instance_allowed": "Slechts een enkele configuratie van Abode is toegestaan." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "invalid_mfa_code": "Ongeldige MFA-code" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "MFA-code (6-cijfers)" + }, + "title": "Voer uw MFA-code voor Abode in" + }, + "reauth_confirm": { + "data": { + "password": "Wachtwoord", + "username": "E-mail" + }, + "title": "Vul uw Abode-inloggegevens in" + }, + "user": { + "data": { + "password": "Wachtwoord", + "username": "E-mailadres" + }, + "title": "Vul uw Abode-inloggegevens in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/no.json new file mode 100644 index 00000000000..27706c3d797 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/no.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "invalid_mfa_code": "Ugyldig MFA-kode" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "MFA-kode (6-sifre)" + }, + "title": "Skriv inn din MFA-kode for Abode" + }, + "reauth_confirm": { + "data": { + "password": "Passord", + "username": "E-post" + }, + "title": "Fyll ut innloggingsinformasjonen for Abode" + }, + "user": { + "data": { + "password": "Passord", + "username": "E-post" + }, + "title": "Fyll ut innloggingsinformasjonen for Abode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/pl.json new file mode 100644 index 00000000000..79966f14d9c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/pl.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_mfa_code": "Nieprawid\u0142owy kod uwierzytelniania wielosk\u0142adnikowego" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "6-cyfrowy kod uwierzytelniania wielosk\u0142adnikowego" + }, + "title": "Wprowad\u017a kod uwierzytelniania wielosk\u0142adnikowego dla Abode" + }, + "reauth_confirm": { + "data": { + "password": "Has\u0142o", + "username": "Adres e-mail" + }, + "title": "Wprowad\u017a informacje logowania Abode" + }, + "user": { + "data": { + "password": "Has\u0142o", + "username": "Adres e-mail" + }, + "title": "Wprowad\u017a informacje logowania Abode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/pt-BR.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/pt-BR.json new file mode 100644 index 00000000000..4c61f5d243d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/pt-BR.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Somente uma \u00fanica configura\u00e7\u00e3o de Abode \u00e9 permitida." + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Endere\u00e7o de e-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/pt.json new file mode 100644 index 00000000000..95a51741222 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/pt.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe", + "username": "Email" + } + }, + "user": { + "data": { + "password": "Palavra-passe", + "username": "Endere\u00e7o de e-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/ro.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/ro.json new file mode 100644 index 00000000000..0b5f3c35ea7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/ro.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_successful": "Re-autentificare efectuata cu succes" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/ru.json new file mode 100644 index 00000000000..f3804a840ab --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/ru.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "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.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_mfa_code": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u0434 MFA." + }, + "step": { + "mfa": { + "data": { + "mfa_code": "\u041a\u043e\u0434 MFA (6 \u0446\u0438\u0444\u0440)" + }, + "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 MFA \u0434\u043b\u044f Abode" + }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "title": "Abode" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "title": "Abode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/sl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/sl.json new file mode 100644 index 00000000000..3f6a142e281 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/sl.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "reauth_successful": "Ponovno overjanje je uspelo", + "single_instance_allowed": "Dovoljena je samo ena konfiguracija Abode." + }, + "error": { + "invalid_mfa_code": "Napa\u010dna MFA koda" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "MFA koda (6 \u0161tevilk)" + }, + "title": "Vnesite MFA kodo za Abode" + }, + "reauth_confirm": { + "data": { + "password": "Geslo", + "username": "E-po\u0161tni naslov" + }, + "title": "Vnesite podatke za prijavo v Abode" + }, + "user": { + "data": { + "password": "Geslo", + "username": "E-po\u0161tni naslov" + }, + "title": "Izpolnite svoje podatke za prijavo v Abode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/sv.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/sv.json new file mode 100644 index 00000000000..9faf392be51 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Endast en enda konfiguration av Abode \u00e4r till\u00e5ten." + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "E-postadress" + }, + "title": "Fyll i din inloggningsinformation f\u00f6r Abode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/th.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/th.json new file mode 100644 index 00000000000..2b9eefdbb6b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/th.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "mfa": { + "title": "\u0e1b\u0e49\u0e2d\u0e19\u0e23\u0e2b\u0e31\u0e2a MFA \u0e08\u0e32\u0e01 Abode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/tr.json new file mode 100644 index 00000000000..d469e43f1f4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/tr.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_mfa_code": "Ge\u00e7ersiz MFA kodu" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "MFA kodu (6 basamakl\u0131)" + }, + "title": "Abode i\u00e7in MFA kodunuzu girin" + }, + "reauth_confirm": { + "data": { + "password": "Parola", + "username": "E-posta" + }, + "title": "Abode giri\u015f bilgilerinizi doldurun" + }, + "user": { + "data": { + "password": "Parola", + "username": "E-posta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/uk.json new file mode 100644 index 00000000000..7ad57a0ec68 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/uk.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "invalid_mfa_code": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439 \u043a\u043e\u0434 MFA." + }, + "step": { + "mfa": { + "data": { + "mfa_code": "\u041a\u043e\u0434 MFA (6 \u0446\u0438\u0444\u0440)" + }, + "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043e\u0434 MFA \u0434\u043b\u044f Abode" + }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + }, + "title": "Abode" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + }, + "title": "Abode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/zh-Hant.json new file mode 100644 index 00000000000..6725df44451 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/abode/translations/zh-Hant.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_mfa_code": "\u591a\u6b65\u9a5f\u8a8d\u8b49\u78bc\u7121\u6548" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "\u591a\u6b65\u9a5f\u8a8d\u8b49\u78bc\uff086 \u4f4d\uff09" + }, + "title": "\u8f38\u5165 Abode \u591a\u6b65\u9a5f\u8a8d\u8b49\u78bc" + }, + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6" + }, + "title": "\u586b\u5beb Abode \u767b\u5165\u8cc7\u8a0a" + }, + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6" + }, + "title": "\u586b\u5beb Abode \u767b\u5165\u8cc7\u8a0a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/__init__.py new file mode 100644 index 00000000000..f6f124b2d4d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/__init__.py @@ -0,0 +1,112 @@ +"""The AccuWeather component.""" +from datetime import timedelta +import logging + +from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError +from aiohttp.client_exceptions import ClientConnectorError +from async_timeout import timeout + +from homeassistant.const import CONF_API_KEY +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + ATTR_FORECAST, + CONF_FORECAST, + COORDINATOR, + DOMAIN, + UNDO_UPDATE_LISTENER, +) + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["sensor", "weather"] + + +async def async_setup_entry(hass, config_entry) -> bool: + """Set up AccuWeather as config entry.""" + api_key = config_entry.data[CONF_API_KEY] + location_key = config_entry.unique_id + forecast = config_entry.options.get(CONF_FORECAST, False) + + _LOGGER.debug("Using location_key: %s, get forecast: %s", location_key, forecast) + + websession = async_get_clientsession(hass) + + coordinator = AccuWeatherDataUpdateCoordinator( + hass, websession, api_key, location_key, forecast + ) + await coordinator.async_config_entry_first_refresh() + + undo_listener = config_entry.add_update_listener(update_listener) + + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = { + COORDINATOR: coordinator, + UNDO_UPDATE_LISTENER: undo_listener, + } + + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + + hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() + + if unload_ok: + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok + + +async def update_listener(hass, config_entry): + """Update listener.""" + await hass.config_entries.async_reload(config_entry.entry_id) + + +class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching AccuWeather data API.""" + + def __init__(self, hass, session, api_key, location_key, forecast: bool): + """Initialize.""" + self.location_key = location_key + self.forecast = forecast + self.is_metric = hass.config.units.is_metric + self.accuweather = AccuWeather(api_key, session, location_key=self.location_key) + + # Enabling the forecast download increases the number of requests per data + # update, we use 40 minutes for current condition only and 80 minutes for + # current condition and forecast as update interval to not exceed allowed number + # of requests. We have 50 requests allowed per day, so we use 36 and leave 14 as + # a reserve for restarting HA. + update_interval = timedelta(minutes=40) + if self.forecast: + update_interval *= 2 + _LOGGER.debug("Data will be update every %s", update_interval) + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + + async def _async_update_data(self): + """Update data via library.""" + try: + async with timeout(10): + current = await self.accuweather.async_get_current_conditions() + forecast = ( + await self.accuweather.async_get_forecast(metric=self.is_metric) + if self.forecast + else {} + ) + except ( + ApiError, + ClientConnectorError, + InvalidApiKeyError, + RequestsExceededError, + ) as error: + raise UpdateFailed(error) from error + _LOGGER.debug("Requests remaining: %s", self.accuweather.requests_remaining) + return {**current, **{ATTR_FORECAST: forecast}} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/config_flow.py new file mode 100644 index 00000000000..999a54b11a7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/config_flow.py @@ -0,0 +1,111 @@ +"""Adds config flow for AccuWeather.""" +import asyncio + +from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError +from aiohttp import ClientError +from aiohttp.client_exceptions import ClientConnectorError +from async_timeout import timeout +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +from .const import CONF_FORECAST, DOMAIN + + +class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for AccuWeather.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + # Under the terms of use of the API, one user can use one free API key. Due to + # the small number of requests allowed, we only allow one integration instance. + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + errors = {} + + if user_input is not None: + websession = async_get_clientsession(self.hass) + try: + async with timeout(10): + accuweather = AccuWeather( + user_input[CONF_API_KEY], + websession, + latitude=user_input[CONF_LATITUDE], + longitude=user_input[CONF_LONGITUDE], + ) + await accuweather.async_get_location() + except (ApiError, ClientConnectorError, asyncio.TimeoutError, ClientError): + errors["base"] = "cannot_connect" + except InvalidApiKeyError: + errors[CONF_API_KEY] = "invalid_api_key" + except RequestsExceededError: + errors[CONF_API_KEY] = "requests_exceeded" + else: + await self.async_set_unique_id( + accuweather.location_key, raise_on_progress=False + ) + + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_API_KEY): str, + vol.Optional( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Optional( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + vol.Optional( + CONF_NAME, default=self.hass.config.location_name + ): str, + } + ), + errors=errors, + ) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Options callback for AccuWeather.""" + return AccuWeatherOptionsFlowHandler(config_entry) + + +class AccuWeatherOptionsFlowHandler(config_entries.OptionsFlow): + """Config flow options for AccuWeather.""" + + def __init__(self, config_entry): + """Initialize AccuWeather options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + return await self.async_step_user() + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Optional( + CONF_FORECAST, + default=self.config_entry.options.get(CONF_FORECAST, False), + ): bool + } + ), + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/const.py new file mode 100644 index 00000000000..60fdd48c8f4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/const.py @@ -0,0 +1,315 @@ +"""Constants for AccuWeather integration.""" +from homeassistant.components.weather import ( + ATTR_CONDITION_CLEAR_NIGHT, + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_EXCEPTIONAL, + ATTR_CONDITION_FOG, + ATTR_CONDITION_HAIL, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_CONDITION_WINDY, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + CONCENTRATION_PARTS_PER_CUBIC_METER, + DEVICE_CLASS_TEMPERATURE, + LENGTH_FEET, + LENGTH_INCHES, + LENGTH_METERS, + LENGTH_MILLIMETERS, + PERCENTAGE, + SPEED_KILOMETERS_PER_HOUR, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TIME_HOURS, + UV_INDEX, +) + +ATTRIBUTION = "Data provided by AccuWeather" +ATTR_FORECAST = CONF_FORECAST = "forecast" +ATTR_LABEL = "label" +ATTR_UNIT_IMPERIAL = "Imperial" +ATTR_UNIT_METRIC = "Metric" +COORDINATOR = "coordinator" +DOMAIN = "accuweather" +MANUFACTURER = "AccuWeather, Inc." +NAME = "AccuWeather" +UNDO_UPDATE_LISTENER = "undo_update_listener" + +CONDITION_CLASSES = { + ATTR_CONDITION_CLEAR_NIGHT: [33, 34, 37], + ATTR_CONDITION_CLOUDY: [7, 8, 38], + ATTR_CONDITION_EXCEPTIONAL: [24, 30, 31], + ATTR_CONDITION_FOG: [11], + ATTR_CONDITION_HAIL: [25], + ATTR_CONDITION_LIGHTNING: [15], + ATTR_CONDITION_LIGHTNING_RAINY: [16, 17, 41, 42], + ATTR_CONDITION_PARTLYCLOUDY: [3, 4, 6, 35, 36], + ATTR_CONDITION_POURING: [18], + ATTR_CONDITION_RAINY: [12, 13, 14, 26, 39, 40], + ATTR_CONDITION_SNOWY: [19, 20, 21, 22, 23, 43, 44], + ATTR_CONDITION_SNOWY_RAINY: [29], + ATTR_CONDITION_SUNNY: [1, 2, 5], + ATTR_CONDITION_WINDY: [32], +} + +FORECAST_DAYS = [0, 1, 2, 3, 4] + +FORECAST_SENSOR_TYPES = { + "CloudCoverDay": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-cloudy", + ATTR_LABEL: "Cloud Cover Day", + ATTR_UNIT_METRIC: PERCENTAGE, + ATTR_UNIT_IMPERIAL: PERCENTAGE, + }, + "CloudCoverNight": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-cloudy", + ATTR_LABEL: "Cloud Cover Night", + ATTR_UNIT_METRIC: PERCENTAGE, + ATTR_UNIT_IMPERIAL: PERCENTAGE, + }, + "Grass": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:grass", + ATTR_LABEL: "Grass Pollen", + ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, + }, + "HoursOfSun": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-partly-cloudy", + ATTR_LABEL: "Hours Of Sun", + ATTR_UNIT_METRIC: TIME_HOURS, + ATTR_UNIT_IMPERIAL: TIME_HOURS, + }, + "Mold": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_LABEL: "Mold Pollen", + ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, + }, + "Ozone": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:vector-triangle", + ATTR_LABEL: "Ozone", + ATTR_UNIT_METRIC: None, + ATTR_UNIT_IMPERIAL: None, + }, + "Ragweed": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:sprout", + ATTR_LABEL: "Ragweed Pollen", + ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, + }, + "RealFeelTemperatureMax": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "RealFeel Temperature Max", + ATTR_UNIT_METRIC: TEMP_CELSIUS, + ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + }, + "RealFeelTemperatureMin": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "RealFeel Temperature Min", + ATTR_UNIT_METRIC: TEMP_CELSIUS, + ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + }, + "RealFeelTemperatureShadeMax": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "RealFeel Temperature Shade Max", + ATTR_UNIT_METRIC: TEMP_CELSIUS, + ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + }, + "RealFeelTemperatureShadeMin": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "RealFeel Temperature Shade Min", + ATTR_UNIT_METRIC: TEMP_CELSIUS, + ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + }, + "ThunderstormProbabilityDay": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-lightning", + ATTR_LABEL: "Thunderstorm Probability Day", + ATTR_UNIT_METRIC: PERCENTAGE, + ATTR_UNIT_IMPERIAL: PERCENTAGE, + }, + "ThunderstormProbabilityNight": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-lightning", + ATTR_LABEL: "Thunderstorm Probability Night", + ATTR_UNIT_METRIC: PERCENTAGE, + ATTR_UNIT_IMPERIAL: PERCENTAGE, + }, + "Tree": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:tree-outline", + ATTR_LABEL: "Tree Pollen", + ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, + }, + "UVIndex": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-sunny", + ATTR_LABEL: "UV Index", + ATTR_UNIT_METRIC: UV_INDEX, + ATTR_UNIT_IMPERIAL: UV_INDEX, + }, + "WindGustDay": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind Gust Day", + ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + }, + "WindGustNight": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind Gust Night", + ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + }, + "WindDay": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind Day", + ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + }, + "WindNight": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind Night", + ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + }, +} + +OPTIONAL_SENSORS = ( + "ApparentTemperature", + "CloudCover", + "CloudCoverDay", + "CloudCoverNight", + "DewPoint", + "Grass", + "Mold", + "Ozone", + "Ragweed", + "RealFeelTemperatureShade", + "RealFeelTemperatureShadeMax", + "RealFeelTemperatureShadeMin", + "Tree", + "WetBulbTemperature", + "WindChillTemperature", + "WindGust", + "WindGustDay", + "WindGustNight", +) + +SENSOR_TYPES = { + "ApparentTemperature": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "Apparent Temperature", + ATTR_UNIT_METRIC: TEMP_CELSIUS, + ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + }, + "Ceiling": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-fog", + ATTR_LABEL: "Cloud Ceiling", + ATTR_UNIT_METRIC: LENGTH_METERS, + ATTR_UNIT_IMPERIAL: LENGTH_FEET, + }, + "CloudCover": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-cloudy", + ATTR_LABEL: "Cloud Cover", + ATTR_UNIT_METRIC: PERCENTAGE, + ATTR_UNIT_IMPERIAL: PERCENTAGE, + }, + "DewPoint": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "Dew Point", + ATTR_UNIT_METRIC: TEMP_CELSIUS, + ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + }, + "RealFeelTemperature": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "RealFeel Temperature", + ATTR_UNIT_METRIC: TEMP_CELSIUS, + ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + }, + "RealFeelTemperatureShade": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "RealFeel Temperature Shade", + ATTR_UNIT_METRIC: TEMP_CELSIUS, + ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + }, + "Precipitation": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-rainy", + ATTR_LABEL: "Precipitation", + ATTR_UNIT_METRIC: LENGTH_MILLIMETERS, + ATTR_UNIT_IMPERIAL: LENGTH_INCHES, + }, + "PressureTendency": { + ATTR_DEVICE_CLASS: "accuweather__pressure_tendency", + ATTR_ICON: "mdi:gauge", + ATTR_LABEL: "Pressure Tendency", + ATTR_UNIT_METRIC: None, + ATTR_UNIT_IMPERIAL: None, + }, + "UVIndex": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-sunny", + ATTR_LABEL: "UV Index", + ATTR_UNIT_METRIC: UV_INDEX, + ATTR_UNIT_IMPERIAL: UV_INDEX, + }, + "WetBulbTemperature": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "Wet Bulb Temperature", + ATTR_UNIT_METRIC: TEMP_CELSIUS, + ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + }, + "WindChillTemperature": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "Wind Chill Temperature", + ATTR_UNIT_METRIC: TEMP_CELSIUS, + ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, + }, + "Wind": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind", + ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + }, + "WindGust": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind Gust", + ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + }, +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/manifest.json new file mode 100644 index 00000000000..04b1b4b39c6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "accuweather", + "name": "AccuWeather", + "documentation": "https://www.home-assistant.io/integrations/accuweather/", + "requirements": ["accuweather==0.2.0"], + "codeowners": ["@bieniu"], + "config_flow": true, + "quality_scale": "platinum", + "iot_class": "cloud_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/sensor.py new file mode 100644 index 00000000000..722dd8869be --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/sensor.py @@ -0,0 +1,166 @@ +"""Support for the AccuWeather service.""" +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_DEVICE_CLASS, + CONF_NAME, + DEVICE_CLASS_TEMPERATURE, +) +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ( + ATTR_FORECAST, + ATTR_ICON, + ATTR_LABEL, + ATTRIBUTION, + COORDINATOR, + DOMAIN, + FORECAST_DAYS, + FORECAST_SENSOR_TYPES, + MANUFACTURER, + NAME, + OPTIONAL_SENSORS, + SENSOR_TYPES, +) + +PARALLEL_UPDATES = 1 + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add AccuWeather entities from a config_entry.""" + name = config_entry.data[CONF_NAME] + + coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] + + sensors = [] + for sensor in SENSOR_TYPES: + sensors.append(AccuWeatherSensor(name, sensor, coordinator)) + + if coordinator.forecast: + for sensor in FORECAST_SENSOR_TYPES: + for day in FORECAST_DAYS: + # Some air quality/allergy sensors are only available for certain + # locations. + if sensor in coordinator.data[ATTR_FORECAST][0]: + sensors.append( + AccuWeatherSensor(name, sensor, coordinator, forecast_day=day) + ) + + async_add_entities(sensors, False) + + +class AccuWeatherSensor(CoordinatorEntity, SensorEntity): + """Define an AccuWeather entity.""" + + def __init__(self, name, kind, coordinator, forecast_day=None): + """Initialize.""" + super().__init__(coordinator) + self._name = name + self.kind = kind + self._device_class = None + self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} + self._unit_system = "Metric" if self.coordinator.is_metric else "Imperial" + self.forecast_day = forecast_day + + @property + def name(self): + """Return the name.""" + if self.forecast_day is not None: + return f"{self._name} {FORECAST_SENSOR_TYPES[self.kind][ATTR_LABEL]} {self.forecast_day}d" + return f"{self._name} {SENSOR_TYPES[self.kind][ATTR_LABEL]}" + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + if self.forecast_day is not None: + return f"{self.coordinator.location_key}-{self.kind}-{self.forecast_day}".lower() + return f"{self.coordinator.location_key}-{self.kind}".lower() + + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self.coordinator.location_key)}, + "name": NAME, + "manufacturer": MANUFACTURER, + "entry_type": "service", + } + + @property + def state(self): + """Return the state.""" + if self.forecast_day is not None: + if ( + FORECAST_SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] + == DEVICE_CLASS_TEMPERATURE + ): + return self.coordinator.data[ATTR_FORECAST][self.forecast_day][ + self.kind + ]["Value"] + if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]: + return self.coordinator.data[ATTR_FORECAST][self.forecast_day][ + self.kind + ]["Speed"]["Value"] + if self.kind in ["Grass", "Mold", "Ragweed", "Tree", "UVIndex", "Ozone"]: + return self.coordinator.data[ATTR_FORECAST][self.forecast_day][ + self.kind + ]["Value"] + return self.coordinator.data[ATTR_FORECAST][self.forecast_day][self.kind] + if self.kind == "Ceiling": + return round(self.coordinator.data[self.kind][self._unit_system]["Value"]) + if self.kind == "PressureTendency": + return self.coordinator.data[self.kind]["LocalizedText"].lower() + if SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE: + return self.coordinator.data[self.kind][self._unit_system]["Value"] + if self.kind == "Precipitation": + return self.coordinator.data["PrecipitationSummary"][self.kind][ + self._unit_system + ]["Value"] + if self.kind in ["Wind", "WindGust"]: + return self.coordinator.data[self.kind]["Speed"][self._unit_system]["Value"] + return self.coordinator.data[self.kind] + + @property + def icon(self): + """Return the icon.""" + if self.forecast_day is not None: + return FORECAST_SENSOR_TYPES[self.kind][ATTR_ICON] + return SENSOR_TYPES[self.kind][ATTR_ICON] + + @property + def device_class(self): + """Return the device_class.""" + if self.forecast_day is not None: + return FORECAST_SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] + return SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + if self.forecast_day is not None: + return FORECAST_SENSOR_TYPES[self.kind][self._unit_system] + return SENSOR_TYPES[self.kind][self._unit_system] + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + if self.forecast_day is not None: + if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]: + self._attrs["direction"] = self.coordinator.data[ATTR_FORECAST][ + self.forecast_day + ][self.kind]["Direction"]["English"] + elif self.kind in ["Grass", "Mold", "Ragweed", "Tree", "UVIndex", "Ozone"]: + self._attrs["level"] = self.coordinator.data[ATTR_FORECAST][ + self.forecast_day + ][self.kind]["Category"] + return self._attrs + if self.kind == "UVIndex": + self._attrs["level"] = self.coordinator.data["UVIndexText"] + elif self.kind == "Precipitation": + self._attrs["type"] = self.coordinator.data["PrecipitationType"] + return self._attrs + + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + return bool(self.kind not in OPTIONAL_SENSORS) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/strings.json new file mode 100644 index 00000000000..c4305a0a7a5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/strings.json @@ -0,0 +1,41 @@ +{ + "config": { + "step": { + "user": { + "title": "AccuWeather", + "description": "If you need help with the configuration have a look here: https://www.home-assistant.io/integrations/accuweather/\n\nSome sensors are not enabled by default. You can enable them in the entity registry after the integration configuration.\nWeather forecast is not enabled by default. You can enable it in the integration options.", + "data": { + "name": "[%key:common::config_flow::data::name%]", + "api_key": "[%key:common::config_flow::data::api_key%]", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "requests_exceeded": "The allowed number of requests to Accuweather API has been exceeded. You have to wait or change API Key." + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + }, + "options": { + "step": { + "user": { + "title": "AccuWeather Options", + "description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 80 minutes instead of every 40 minutes.", + "data": { + "forecast": "Weather forecast" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Reach AccuWeather server", + "remaining_requests": "Remaining allowed requests" + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/strings.sensor.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/strings.sensor.json new file mode 100644 index 00000000000..57cb89bcecf --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/strings.sensor.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "steady": "Steady", + "rising": "Rising", + "falling": "Falling" + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/system_health.py b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/system_health.py new file mode 100644 index 00000000000..58c9ba35881 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/system_health.py @@ -0,0 +1,27 @@ +"""Provide info to system health.""" +from accuweather.const import ENDPOINT + +from homeassistant.components import system_health +from homeassistant.core import HomeAssistant, callback + +from .const import COORDINATOR, DOMAIN + + +@callback +def async_register( + hass: HomeAssistant, register: system_health.SystemHealthRegistration +) -> None: + """Register system health callbacks.""" + register.async_register_info(system_health_info) + + +async def system_health_info(hass): + """Get info for the info page.""" + remaining_requests = list(hass.data[DOMAIN].values())[0][ + COORDINATOR + ].accuweather.requests_remaining + + return { + "can_reach_server": system_health.async_check_can_reach_url(hass, ENDPOINT), + "remaining_requests": remaining_requests, + } diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/ca.json new file mode 100644 index 00000000000..8178a5caef0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/ca.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_api_key": "Clau API inv\u00e0lida", + "requests_exceeded": "S'ha superat el nombre m\u00e0xim de sol\u00b7licituds permeses a l'API d'AccuWeather. Has d'esperar-te o canviar la clau API." + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nom" + }, + "description": "Si necessites ajuda amb la configuraci\u00f3, consulta els seg\u00fcent enlla\u00e7: https://www.home-assistant.io/integrations/accuweather/ \n\n Alguns sensors no estan activats de manera predeterminada. Els pots activar des del registre d'entitats, despr\u00e9s de la configurraci\u00f3 de la integraci\u00f3.\n La previsi\u00f3 meteorol\u00f2gica no est\u00e0 activada de manera predeterminada. Pots activar-la en les opcions de la integraci\u00f3.", + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Previsi\u00f3 meteorol\u00f2gica" + }, + "description": "Per culpa de les limitacions de la versi\u00f3 gratu\u00efta l'API d'AccuWeather, quan habilitis la previsi\u00f3 meteorol\u00f2gica, les actualitzacions de dades es faran cada 80 minuts en comptes de cada 40.", + "title": "Opcions d'AccuWeather" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Servidor d'Accuweather accessible", + "remaining_requests": "Sol\u00b7licituds permeses restants" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/cs.json new file mode 100644 index 00000000000..1cf34a42695 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/cs.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API", + "requests_exceeded": "Byl p\u0159ekro\u010den povolen\u00fd po\u010det po\u017eadavk\u016f pro API Accuweather. Mus\u00edte po\u010dkat nebo zm\u011bnit API kl\u00ed\u010d." + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", + "name": "Jm\u00e9no" + }, + "description": "Pokud pot\u0159ebujete pomoc s nastaven\u00ed, pod\u00edvejte se na: https://www.home-assistant.io/integrations/accuweather/\n\nN\u011bkter\u00e9 senzory nejsou ve v\u00fdchoz\u00edm nastaven\u00ed povoleny. M\u016f\u017eete je povolit po nastaven\u00ed integrace v registru entit.\nP\u0159edpov\u011b\u010f po\u010das\u00ed nen\u00ed ve v\u00fdchoz\u00edm nastaven\u00ed povolena. M\u016f\u017eete ji povolit v mo\u017enostech integrace.", + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "P\u0159edpov\u011b\u010f po\u010das\u00ed" + }, + "description": "Kdy\u017e povol\u00edte p\u0159edpov\u011b\u010f po\u010das\u00ed, budou aktualizace dat prov\u00e1d\u011bny ka\u017ed\u00fdch 80 minut nam\u00edsto 40 minut z d\u016fvodu omezen\u00ed bezplatn\u00e9 verze AccuWeather.", + "title": "Mo\u017enosti AccuWeather" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Lze kontaktovat AccuWeather server", + "remaining_requests": "Zb\u00fdvaj\u00edc\u00ed povolen\u00e9 \u017e\u00e1dosti" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/de.json new file mode 100644 index 00000000000..a9b23bacf6c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/de.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", + "requests_exceeded": "Die zul\u00e4ssige Anzahl von Anforderungen an die Accuweather-API wurde \u00fcberschritten. Sie m\u00fcssen warten oder den API-Schl\u00fcssel \u00e4ndern." + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "name": "Name" + }, + "description": "Wenn du Hilfe bei der Konfiguration ben\u00f6tigst, schaue hier nach: https://www.home-assistant.io/integrations/accuweather/\n\nEinige Sensoren sind standardm\u00e4\u00dfig nicht aktiviert. Du kannst sie in der Entit\u00e4tsregister nach der Integrationskonfiguration aktivieren.\nDie Wettervorhersage ist nicht standardm\u00e4\u00dfig aktiviert. Du kannst sie in den Integrationsoptionen aktivieren.", + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Wettervorhersage" + }, + "description": "Aufgrund der Einschr\u00e4nkungen der kostenlosen Version des AccuWeather-API-Schl\u00fcssels werden bei aktivierter Wettervorhersage Datenaktualisierungen alle 80 Minuten statt alle 40 Minuten durchgef\u00fchrt.", + "title": "AccuWeather Optionen" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "AccuWeather-Server erreichen", + "remaining_requests": "Verbleibende erlaubte Anfragen" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/en.json new file mode 100644 index 00000000000..8f2261b93c7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/en.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_api_key": "Invalid API key", + "requests_exceeded": "The allowed number of requests to Accuweather API has been exceeded. You have to wait or change API Key." + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name" + }, + "description": "If you need help with the configuration have a look here: https://www.home-assistant.io/integrations/accuweather/\n\nSome sensors are not enabled by default. You can enable them in the entity registry after the integration configuration.\nWeather forecast is not enabled by default. You can enable it in the integration options.", + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Weather forecast" + }, + "description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 80 minutes instead of every 40 minutes.", + "title": "AccuWeather Options" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Reach AccuWeather server", + "remaining_requests": "Remaining allowed requests" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/es-419.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/es-419.json new file mode 100644 index 00000000000..92d5d5ef2c2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/es-419.json @@ -0,0 +1,29 @@ +{ + "config": { + "error": { + "requests_exceeded": "Se super\u00f3 el n\u00famero permitido de solicitudes a la API de Accuweather. Tiene que esperar o cambiar la clave de API." + }, + "step": { + "user": { + "description": "Si necesita ayuda con la configuraci\u00f3n, eche un vistazo aqu\u00ed: https://www.home-assistant.io/integrations/accuweather/ \n\nAlgunos sensores no est\u00e1n habilitados de forma predeterminada. Puede habilitarlos en el registro de entidades despu\u00e9s de la configuraci\u00f3n de integraci\u00f3n. La previsi\u00f3n meteorol\u00f3gica no est\u00e1 habilitada de forma predeterminada. Puede habilitarlo en las opciones de integraci\u00f3n.", + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Pron\u00f3stico del tiempo" + }, + "description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilita el pron\u00f3stico del tiempo, las actualizaciones de datos se realizar\u00e1n cada 64 minutos en lugar de cada 32 minutos.", + "title": "Opciones de AccuWeather" + } + } + }, + "system_health": { + "info": { + "remaining_requests": "Solicitudes permitidas restantes" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/es.json new file mode 100644 index 00000000000..aa24b5ff975 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/es.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_api_key": "Clave API no v\u00e1lida", + "requests_exceeded": "Se ha excedido el n\u00famero permitido de solicitudes a la API de Accuweather. Tienes que esperar o cambiar la Clave API." + }, + "step": { + "user": { + "data": { + "api_key": "Clave API", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre" + }, + "description": "Si necesitas ayuda con la configuraci\u00f3n, echa un vistazo aqu\u00ed: https://www.home-assistant.io/integrations/accuweather/ \n\nEl pron\u00f3stico del tiempo no est\u00e1 habilitado por defecto. Puedes habilitarlo en las opciones de la integraci\u00f3n.", + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Pron\u00f3stico del tiempo" + }, + "description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilitas el pron\u00f3stico del tiempo, las actualizaciones de datos se realizar\u00e1n cada 64 minutos en lugar de cada 32 minutos.", + "title": "Opciones de AccuWeather" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Alcanzar el servidor AccuWeather", + "remaining_requests": "Solicitudes permitidas restantes" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/et.json new file mode 100644 index 00000000000..6e2dc1ffd96 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/et.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sidumine juba tehtud. V\u00f5imalik on ainult 1 sidumine." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_api_key": "API v\u00f5ti on vale", + "requests_exceeded": "Accuweatheri API-le esitatud p\u00e4ringute piirarv on \u00fcletatud. Peate ootama (v\u00f5i muutma API v\u00f5tit)." + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad", + "name": "Sidumise nimi" + }, + "description": "Kui vajate seadistamisel abi vaadake siit: https://www.home-assistant.io/integrations/accuweather/ \n\n M\u00f5ni andur pole vaikimisi lubatud. P\u00e4rast sidumise seadistamist saate need \u00fcksused lubada. \n Ilmapennustus pole vaikimisi lubatud. Saate selle lubada sidumise s\u00e4tetes.", + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Ilmateade" + }, + "description": "AccuWeather API tasuta versioonis toimub ilmaennustuse lubamisel andmete v\u00e4rskendamine iga 80 minuti j\u00e4rel (muidu 40 minutit).", + "title": "AccuWeatheri valikud" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u00dchendus Accuweatheri serveriga", + "remaining_requests": "Lubatud taotlusi on j\u00e4\u00e4nud" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/fr.json new file mode 100644 index 00000000000..a083ed09bdf --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/fr.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_api_key": "Cl\u00e9 API invalide", + "requests_exceeded": "Le nombre autoris\u00e9 de requ\u00eates adress\u00e9es \u00e0 l'API AccuWeather a \u00e9t\u00e9 d\u00e9pass\u00e9. Vous devez attendre ou modifier la cl\u00e9 API." + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nom de l'int\u00e9gration" + }, + "description": "Si vous avez besoin d'aide pour la configuration, consultez le site suivant : https://www.home-assistant.io/integrations/accuweather/\n\nCertains capteurs ne sont pas activ\u00e9s par d\u00e9faut. Vous pouvez les activer dans le registre des entit\u00e9s apr\u00e8s la configuration de l'int\u00e9gration.\nLes pr\u00e9visions m\u00e9t\u00e9orologiques ne sont pas activ\u00e9es par d\u00e9faut. Vous pouvez l'activer dans les options d'int\u00e9gration.", + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Pr\u00e9visions m\u00e9t\u00e9orologiques" + }, + "description": "En raison des limitations de la version gratuite de la cl\u00e9 API AccuWeather, lorsque vous activez les pr\u00e9visions m\u00e9t\u00e9orologiques, les mises \u00e0 jour des donn\u00e9es seront effectu\u00e9es toutes les 64 minutes au lieu de toutes les 32 minutes.", + "title": "Options AccuWeather" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Acc\u00e8s au serveur AccuWeather", + "remaining_requests": "Demandes restantes autoris\u00e9es" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/he.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/he.json new file mode 100644 index 00000000000..4c49313d977 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/hu.json new file mode 100644 index 00000000000..8a0f7f5a198 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/hu.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "name": "N\u00e9v" + }, + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "title": "AccuWeather be\u00e1ll\u00edt\u00e1sok" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/id.json new file mode 100644 index 00000000000..970b3a026b7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/id.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_api_key": "Kunci API tidak valid", + "requests_exceeded": "Jumlah permintaan yang diizinkan ke API Accuweather telah terlampaui. Anda harus menunggu atau mengubah Kunci API." + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "latitude": "Lintang", + "longitude": "Bujur", + "name": "Nama" + }, + "description": "Jika Anda memerlukan bantuan tentang konfigurasi, baca di sini: https://www.home-assistant.io/integrations/accuweather/\n\nBeberapa sensor tidak diaktifkan secara default. Anda dapat mengaktifkannya di registri entitas setelah konfigurasi integrasi.\nPrakiraan cuaca tidak diaktifkan secara default. Anda dapat mengaktifkannya di opsi integrasi.", + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Prakiraan cuaca" + }, + "description": "Karena keterbatasan versi gratis kunci API AccuWeather, ketika Anda mengaktifkan prakiraan cuaca, pembaruan data akan dilakukan setiap 80 menit, bukan setiap 40 menit.", + "title": "Opsi AccuWeather" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Keterjangkauan server AccuWeather", + "remaining_requests": "Sisa permintaan yang diizinkan" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/it.json new file mode 100644 index 00000000000..8a1f9b96463 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/it.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_api_key": "Chiave API non valida", + "requests_exceeded": "\u00c8 stato superato il numero consentito di richieste all'API di Accuweather. \u00c8 necessario attendere o modificare la chiave API." + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "latitude": "Latitudine", + "longitude": "Logitudine", + "name": "Nome" + }, + "description": "Se hai bisogno di aiuto con la configurazione dai un'occhiata qui: https://www.home-assistant.io/integrations/accuweather/ \n\nAlcuni sensori non sono abilitati per impostazione predefinita. \u00c8 possibile abilitarli nel registro entit\u00e0 dopo la configurazione di integrazione. \nLe previsioni meteo non sono abilitate per impostazione predefinita. Puoi abilitarle nelle opzioni di integrazione.", + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Previsioni meteo" + }, + "description": "A causa delle limitazioni della versione gratuita della chiave API AccuWeather, quando si abilitano le previsioni del tempo, gli aggiornamenti dei dati verranno eseguiti ogni 80 minuti invece che ogni 40.", + "title": "Opzioni AccuWeather" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Raggiungi il server AccuWeather", + "remaining_requests": "Richieste consentite rimanenti" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/ko.json new file mode 100644 index 00000000000..d992d0bfdd4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/ko.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "requests_exceeded": "Accuweather API\uc5d0 \ud5c8\uc6a9\ub41c \uc694\uccad \uc218\uac00 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uae30\ub2e4\ub9ac\uac70\ub098 API \ud0a4\ub97c \ubcc0\uacbd\ud574\uc57c \ud569\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "api_key": "API \ud0a4", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "name": "\uc774\ub984" + }, + "description": "\uad6c\uc131\uc5d0 \ub300\ud55c \ub3c4\uc6c0\uc774 \ud544\uc694\ud55c \uacbd\uc6b0 \ub2e4\uc74c\uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694: https://www.home-assistant.io/integrations/accuweather/\n\n\uc77c\ubd80 \uc13c\uc11c\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uad6c\uc131 \ud6c4 \uad6c\uc131\uc694\uc18c \ub808\uc9c0\uc2a4\ud2b8\ub9ac\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\n\uc77c\uae30\uc608\ubcf4\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc635\uc158\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "\ub0a0\uc528 \uc608\ubcf4" + }, + "description": "\ubb34\ub8cc \ubc84\uc804\uc758 AccuWeather API \ud0a4\ub85c \uc77c\uae30\uc608\ubcf4\ub97c \ud65c\uc131\ud654\ud55c \uacbd\uc6b0 \uc81c\ud55c\uc0ac\ud56d\uc73c\ub85c \uc778\ud574 \uc5c5\ub370\uc774\ud2b8\ub294 40 \ubd84\uc774 \uc544\ub2cc 80 \ubd84\ub9c8\ub2e4 \uc218\ud589\ub429\ub2c8\ub2e4.", + "title": "AccuWeather \uc635\uc158" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "AccuWeather \uc11c\ubc84 \uc5f0\uacb0", + "remaining_requests": "\ub0a8\uc740 \ud5c8\uc6a9 \uc694\uccad \ud69f\uc218" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/lb.json new file mode 100644 index 00000000000..7f3855a7b9c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/lb.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_api_key": "Ong\u00ebltegen API Schl\u00ebssel", + "requests_exceeded": "D\u00e9i zougelooss Zuel vun Ufroen un Accuweather API gouf iwwerschratt. Du muss ofwaarden oder den API Schl\u00ebssel \u00e4nneren." + }, + "step": { + "user": { + "data": { + "api_key": "API Schl\u00ebssel", + "latitude": "Breedegrad", + "longitude": "L\u00e4ngegrad", + "name": "Numm" + }, + "description": "Falls du H\u00ebllef mat der Konfiguratioun brauch kuck h\u00e9i:\nhttps://www.home-assistant.io/integrations/accuweather/\n\nVerschidde Sensoren si standardm\u00e9isseg net aktiv. Du kanns d\u00e9i an der Entit\u00e9ie Registry no der Konfiguratioun vun der Integratioun aschalten.\n\nWieder Pr\u00e9visounen si standardm\u00e9isseg net aktiv. Du kanns d\u00e9i an den Optioune vun der Integratioun aschalten.", + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Wieder Pr\u00e9visioun" + }, + "description": "Duerch d'Limite vun der Gratis Versioun vun der AccuWeather API, wann d'Wieder Pr\u00e9visoune aktiv\u00e9iert sinn, ginn d'Aktualis\u00e9ierungen all 64 Minutten gemaach, am plaatz vun all 32 Minutten.", + "title": "AccuWeather Optiounen" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "AccuWeather Server ereechbar", + "remaining_requests": "Rescht vun erlaabten Ufroen" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/nl.json new file mode 100644 index 00000000000..f04d93b5921 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/nl.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_api_key": "API-sleutel", + "requests_exceeded": "Het toegestane aantal verzoeken aan de Accuweather API is overschreden. U moet wachten of de API-sleutel wijzigen." + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "name": "Naam" + }, + "description": "Als je hulp nodig hebt bij de configuratie, kijk dan hier: https://www.home-assistant.io/integrations/accuweather/ \n\n Sommige sensoren zijn niet standaard ingeschakeld. U kunt ze inschakelen in het entiteitenregister na de integratieconfiguratie.\n Weersvoorspelling is niet standaard ingeschakeld. U kunt het inschakelen in de integratieopties.", + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Weervoorspelling" + }, + "description": "Vanwege de beperkingen van de gratis versie van de AccuWeather API-sleutel, worden gegevensupdates elke 64 minuten in plaats van elke 32 minuten uitgevoerd wanneer u weersvoorspelling inschakelt.", + "title": "AccuWeather-opties" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Kan AccuWeather server bereiken", + "remaining_requests": "Resterende toegestane verzoeken" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/no.json new file mode 100644 index 00000000000..be87b1ab244 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/no.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_api_key": "Ugyldig API-n\u00f8kkel", + "requests_exceeded": "Det tillatte antallet foresp\u00f8rsler til Accuweather API er overskredet. Du m\u00e5 vente eller endre API-n\u00f8kkel." + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "name": "Navn" + }, + "description": "Hvis du trenger hjelp med konfigurasjonen, kan du se her: https://www.home-assistant.io/integrations/accuweather/ \n\nNoen sensorer er ikke aktivert som standard. Du kan aktivere dem i entitetsregisteret etter integrasjonskonfigurasjonen. \nV\u00e6rmelding er ikke aktivert som standard. Du kan aktivere det i integrasjonsalternativene.", + "title": "" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "V\u00e6rmelding" + }, + "description": "P\u00e5 grunn av begrensningene i den gratis versjonen av AccuWeather API-n\u00f8kkelen, vil dataoppdateringer utf\u00f8res hvert 80. minutt i stedet for hvert 40. minutt n\u00e5r du aktiverer v\u00e6rmelding.", + "title": "AccuWeather-alternativer" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "N\u00e5 AccuWeather-serveren", + "remaining_requests": "Gjenv\u00e6rende tillatte foresp\u00f8rsler" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/pl.json new file mode 100644 index 00000000000..2794bc8b7b6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/pl.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_api_key": "Nieprawid\u0142owy klucz API", + "requests_exceeded": "Dozwolona liczba zapyta\u0144 do interfejsu API AccuWeather zosta\u0142a przekroczona. Musisz poczeka\u0107 lub zmieni\u0107 klucz API." + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa" + }, + "description": "Je\u015bli potrzebujesz pomocy z konfiguracj\u0105, przejd\u017a na stron\u0119: https://www.home-assistant.io/integrations/accuweather/ \n\nCz\u0119\u015b\u0107 sensor\u00f3w nie jest w\u0142\u0105czona domy\u015blnie. Mo\u017cesz je w\u0142\u0105czy\u0107 w rejestrze encji po konfiguracji integracji.\nPrognoza pogody nie jest domy\u015blnie w\u0142\u0105czona. Mo\u017cesz j\u0105 w\u0142\u0105czy\u0107 w opcjach integracji.", + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Prognoza pogody" + }, + "description": "Ze wzgl\u0119du na ograniczenia darmowej wersji klucza API AccuWeather po w\u0142\u0105czeniu prognozy pogody aktualizacje danych b\u0119d\u0105 wykonywane co 80 minut zamiast co 40 minut.", + "title": "Opcje AccuWeather" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Dost\u0119p do serwera AccuWeather", + "remaining_requests": "Pozosta\u0142o dozwolonych \u017c\u0105da\u0144" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/pt-BR.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/pt-BR.json new file mode 100644 index 00000000000..75111f9892d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/pt-BR.json @@ -0,0 +1,24 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Chave API", + "latitude": "Latitude", + "longitude": "Longitude" + }, + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Previs\u00e3o do Tempo" + }, + "description": "Devido \u00e0s limita\u00e7\u00f5es da vers\u00e3o gratuita da chave da API AccuWeather, quando voc\u00ea habilita a previs\u00e3o do tempo, as atualiza\u00e7\u00f5es de dados ser\u00e3o realizadas a cada 64 minutos em vez de a cada 32 minutos." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/pt.json new file mode 100644 index 00000000000..14260bd572d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/pt.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_api_key": "Chave de API inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nome" + } + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Previs\u00e3o meteorol\u00f3gica" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/ru.json new file mode 100644 index 00000000000..7bc767b1baf --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/ru.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "requests_exceeded": "\u041f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u043e \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u043a API Accuweather. \u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u043e\u0434\u043e\u0436\u0434\u0430\u0442\u044c \u0438\u043b\u0438 \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438, \u0435\u0441\u043b\u0438 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u0430 \u043f\u043e\u043c\u043e\u0449\u044c \u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439:\nhttps://www.home-assistant.io/integrations/accuweather/ \n\n\u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u044b \u0441\u043a\u0440\u044b\u0442\u044b \u0438 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d \u043f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u044b. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u043d\u0443\u0436\u043d\u044b\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432 \u0432 \u0440\u0435\u0435\u0441\u0442\u0440\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0438 \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u044b \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438.", + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u044b" + }, + "description": "\u0412 \u0441\u0432\u044f\u0437\u0438 \u0441 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f\u043c\u0438 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u043a\u043b\u044e\u0447\u0430 API AccuWeather, \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 \u043f\u043e\u0433\u043e\u0434\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442\u044c \u043a\u0430\u0436\u0434\u044b\u0435 80 \u043c\u0438\u043d\u0443\u0442, \u0430 \u043d\u0435 \u043a\u0430\u0436\u0434\u044b\u0435 40 \u043c\u0438\u043d\u0443\u0442.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AccuWeather" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 AccuWeather", + "remaining_requests": "\u0421\u0447\u0451\u0442\u0447\u0438\u043a \u043e\u0441\u0442\u0430\u0432\u0448\u0438\u0445\u0441\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.ca.json new file mode 100644 index 00000000000..ad6c43a54ca --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.ca.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Disminuint", + "rising": "Augmentant", + "steady": "Estable" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.cs.json new file mode 100644 index 00000000000..e49b09927d5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.cs.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Klesaj\u00edc\u00ed", + "rising": "Roustouc\u00ed", + "steady": "St\u00e1l\u00fd" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.de.json new file mode 100644 index 00000000000..7ccc7c7360a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.de.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Fallend", + "rising": "Steigend", + "steady": "Gleichbleibend" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.en.json new file mode 100644 index 00000000000..8786583686b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.en.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Falling", + "rising": "Rising", + "steady": "Steady" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.es-419.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.es-419.json new file mode 100644 index 00000000000..b4119777260 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.es-419.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Descendente", + "rising": "Creciente", + "steady": "Firme" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.es.json new file mode 100644 index 00000000000..72d666b1ba3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.es.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Cayendo", + "rising": "Subiendo", + "steady": "Estable" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.et.json new file mode 100644 index 00000000000..ca58cd9ab6b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.et.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Langev", + "rising": "T\u00f5usev", + "steady": "\u00dchtlane" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.fr.json new file mode 100644 index 00000000000..cd0a04eceee --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.fr.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "En baisse", + "rising": "En hausse", + "steady": "Stable" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.hu.json new file mode 100644 index 00000000000..49f2fe41ab3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.hu.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Cs\u00f6kken\u0151", + "rising": "Emelked\u0151", + "steady": "\u00c1lland\u00f3" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.id.json new file mode 100644 index 00000000000..8ce99bbc8c3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.id.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Turun", + "rising": "Naik", + "steady": "Tetap" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.it.json new file mode 100644 index 00000000000..9252821b8de --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.it.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Diminuzione", + "rising": "Aumento", + "steady": "Stabile" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.ko.json new file mode 100644 index 00000000000..287974fa3fd --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.ko.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "\ud558\uac15", + "rising": "\uc0c1\uc2b9", + "steady": "\uc548\uc815" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.lb.json new file mode 100644 index 00000000000..b4d90370e7c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.lb.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "R\u00e9ckleefeg", + "rising": "Erh\u00e9ijung", + "steady": "Stabil" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.nl.json new file mode 100644 index 00000000000..4360149ccc4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.nl.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Ondergang", + "rising": "Opkomst", + "steady": "Stabiel" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.no.json new file mode 100644 index 00000000000..abe8a935115 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.no.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Fallende", + "rising": "Stiger", + "steady": "Jevn" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.pl.json new file mode 100644 index 00000000000..cc7ba9b873c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.pl.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "spada", + "rising": "ro\u015bnie", + "steady": "bez zmian" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.ru.json new file mode 100644 index 00000000000..fd791040d9f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.ru.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "\u041f\u043e\u043d\u0438\u0436\u0430\u044e\u0449\u0435\u0435\u0441\u044f", + "rising": "\u041f\u043e\u0432\u044b\u0448\u0430\u044e\u0449\u0435\u0435\u0441\u044f", + "steady": "\u041f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.sv.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.sv.json new file mode 100644 index 00000000000..cc940f75b17 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.sv.json @@ -0,0 +1,8 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Fallande", + "rising": "Stigande" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.uk.json new file mode 100644 index 00000000000..81243e0b05d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.uk.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "\u0417\u043d\u0438\u0436\u0435\u043d\u043d\u044f", + "rising": "\u0417\u0440\u043e\u0441\u0442\u0430\u043d\u043d\u044f", + "steady": "\u0421\u0442\u0430\u0431\u0456\u043b\u044c\u043d\u0438\u0439" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.zh-Hant.json new file mode 100644 index 00000000000..35bc04eaf04 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sensor.zh-Hant.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "\u4e0b\u964d", + "rising": "\u4e0a\u5347", + "steady": "\u7a69\u5b9a" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sl.json new file mode 100644 index 00000000000..f41ee93aefe --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/sl.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "can_reach_server": "Dostop do AccuWeather stre\u017enika", + "remaining_requests": "Preostalo dovoljenih zahtevkov" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/tr.json new file mode 100644 index 00000000000..f79f9a0e327 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/tr.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam", + "name": "Ad" + }, + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Hava Durumu tahmini" + }, + "title": "AccuWeather Se\u00e7enekleri" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "AccuWeather sunucusuna ula\u015f\u0131n", + "remaining_requests": "Kalan izin verilen istekler" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/uk.json new file mode 100644 index 00000000000..7432d0df484 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/uk.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API", + "requests_exceeded": "\u041f\u0435\u0440\u0435\u0432\u0438\u0449\u0435\u043d\u043e \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u0443 \u043a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u0437\u0430\u043f\u0438\u0442\u0456\u0432 \u0434\u043e API Accuweather. \u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u043f\u043e\u0447\u0435\u043a\u0430\u0442\u0438 \u0430\u0431\u043e \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u043a\u043b\u044e\u0447 API." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438, \u044f\u043a\u0449\u043e \u0412\u0430\u043c \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u0430 \u0437 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u043c:\n https://www.home-assistant.io/integrations/accuweather/ \n\n\u0417\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0434\u0435\u044f\u043a\u0456 \u0441\u0435\u043d\u0441\u043e\u0440\u0438 \u043f\u0440\u0438\u0445\u043e\u0432\u0430\u043d\u0456 \u0456 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u043f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u0438. \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0430\u043a\u0442\u0438\u0432\u0443\u0432\u0430\u0442\u0438 \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u0438\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u0456\u0432 \u0432 \u0440\u0435\u0454\u0441\u0442\u0440\u0456 \u043e\u0431'\u0454\u043a\u0442\u0456\u0432 \u0456 \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u0438 \u0432 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u0445 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457.", + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u0438" + }, + "description": "\u0423 \u0437\u0432'\u044f\u0437\u043a\u0443 \u0437 \u043e\u0431\u043c\u0435\u0436\u0435\u043d\u043d\u044f\u043c\u0438 \u0431\u0435\u0437\u043a\u043e\u0448\u0442\u043e\u0432\u043d\u043e\u0457 \u0432\u0435\u0440\u0441\u0456\u0457 \u043a\u043b\u044e\u0447\u0430 API AccuWeather, \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u0456 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0443 \u043f\u043e\u0433\u043e\u0434\u0438 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0434\u0430\u043d\u0438\u0445 \u0431\u0443\u0434\u0435 \u0432\u0456\u0434\u0431\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u043a\u043e\u0436\u043d\u0456 64 \u0445\u0432\u0438\u043b\u0438\u043d\u0438, \u0430 \u043d\u0435 \u043a\u043e\u0436\u043d\u0456 32 \u0445\u0432\u0438\u043b\u0438\u043d\u0438.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AccuWeather" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 AccuWeather", + "remaining_requests": "\u0417\u0430\u043f\u0438\u0442\u0456\u0432 \u0437\u0430\u043b\u0438\u0448\u0438\u043b\u043e\u0441\u044c" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/zh-Hans.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/zh-Hans.json new file mode 100644 index 00000000000..f8879f5715f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/zh-Hans.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "can_reach_server": "\u53ef\u8bbf\u95ee AccuWeather \u670d\u52a1\u5668", + "remaining_requests": "\u5176\u4f59\u5141\u8bb8\u7684\u8bf7\u6c42" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/zh-Hant.json new file mode 100644 index 00000000000..eb3729fd2c4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/translations/zh-Hant.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_api_key": "API \u5bc6\u9470\u7121\u6548", + "requests_exceeded": "\u5df2\u8d85\u904e Accuweather API \u5141\u8a31\u7684\u8acb\u6c42\u6b21\u6578\u3002\u5fc5\u9808\u7b49\u5019\u6216\u8b8a\u66f4 API \u5bc6\u9470\u3002" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u540d\u7a31" + }, + "description": "\u5047\u5982\u4f60\u9700\u8981\u5354\u52a9\u9032\u884c\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1\uff1ahttps://www.home-assistant.io/integrations/accuweather/\n\n\u67d0\u4e9b\u50b3\u611f\u5668\u9810\u8a2d\u70ba\u672a\u555f\u7528\uff0c\u53ef\u4ee5\u65bc\u6574\u5408\u8a2d\u5b9a\u4e2d\u555f\u7528\u9019\u4e9b\u5be6\u9ad4\u3002\u5929\u6c23\u9810\u5831\u9810\u8a2d\u672a\u958b\u555f\u3002\u53ef\u4ee5\u65bc\u6574\u5408\u9078\u9805\u4e2d\u958b\u555f\u3002", + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "\u5929\u6c23\u9810\u5831" + }, + "description": "\u7531\u65bc AccuWeather API \u5bc6\u9470\u514d\u8cbb\u7248\u672c\u9650\u5236\uff0c\u7576\u958b\u555f\u5929\u6c23\u9810\u5831\u6642\u3001\u6578\u64da\u6703\u6bcf 80 \u5206\u9418\u66f4\u65b0\u4e00\u6b21\uff0c\u800c\u975e 40 \u5206\u9418\u3002", + "title": "AccuWeather \u9078\u9805" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u9023\u7dda AccuWeather \u4f3a\u670d\u5668", + "remaining_requests": "\u5176\u9918\u5141\u8a31\u7684\u8acb\u6c42" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/weather.py b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/weather.py new file mode 100644 index 00000000000..3c0dcfedf43 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/accuweather/weather.py @@ -0,0 +1,176 @@ +"""Support for the AccuWeather service.""" +from statistics import mean + +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, + WeatherEntity, +) +from homeassistant.const import CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util.dt import utc_from_timestamp + +from .const import ( + ATTR_FORECAST, + ATTRIBUTION, + CONDITION_CLASSES, + COORDINATOR, + DOMAIN, + MANUFACTURER, + NAME, +) + +PARALLEL_UPDATES = 1 + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add a AccuWeather weather entity from a config_entry.""" + name = config_entry.data[CONF_NAME] + + coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] + + async_add_entities([AccuWeatherEntity(name, coordinator)], False) + + +class AccuWeatherEntity(CoordinatorEntity, WeatherEntity): + """Define an AccuWeather entity.""" + + def __init__(self, name, coordinator): + """Initialize.""" + super().__init__(coordinator) + self._name = name + self._attrs = {} + self._unit_system = "Metric" if self.coordinator.is_metric else "Imperial" + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return self.coordinator.location_key + + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self.coordinator.location_key)}, + "name": NAME, + "manufacturer": MANUFACTURER, + "entry_type": "service", + } + + @property + def condition(self): + """Return the current condition.""" + try: + return [ + k + for k, v in CONDITION_CLASSES.items() + if self.coordinator.data["WeatherIcon"] in v + ][0] + except IndexError: + return None + + @property + def temperature(self): + """Return the temperature.""" + return self.coordinator.data["Temperature"][self._unit_system]["Value"] + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS if self.coordinator.is_metric else TEMP_FAHRENHEIT + + @property + def pressure(self): + """Return the pressure.""" + return self.coordinator.data["Pressure"][self._unit_system]["Value"] + + @property + def humidity(self): + """Return the humidity.""" + return self.coordinator.data["RelativeHumidity"] + + @property + def wind_speed(self): + """Return the wind speed.""" + return self.coordinator.data["Wind"]["Speed"][self._unit_system]["Value"] + + @property + def wind_bearing(self): + """Return the wind bearing.""" + return self.coordinator.data["Wind"]["Direction"]["Degrees"] + + @property + def visibility(self): + """Return the visibility.""" + return self.coordinator.data["Visibility"][self._unit_system]["Value"] + + @property + def ozone(self): + """Return the ozone level.""" + # We only have ozone data for certain locations and only in the forecast data. + if self.coordinator.forecast and self.coordinator.data[ATTR_FORECAST][0].get( + "Ozone" + ): + return self.coordinator.data[ATTR_FORECAST][0]["Ozone"]["Value"] + return None + + @property + def forecast(self): + """Return the forecast array.""" + if not self.coordinator.forecast: + return None + # remap keys from library to keys understood by the weather component + forecast = [ + { + ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(), + ATTR_FORECAST_TEMP: item["TemperatureMax"]["Value"], + ATTR_FORECAST_TEMP_LOW: item["TemperatureMin"]["Value"], + ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(item), + ATTR_FORECAST_PRECIPITATION_PROBABILITY: round( + mean( + [ + item["PrecipitationProbabilityDay"], + item["PrecipitationProbabilityNight"], + ] + ) + ), + ATTR_FORECAST_WIND_SPEED: item["WindDay"]["Speed"]["Value"], + ATTR_FORECAST_WIND_BEARING: item["WindDay"]["Direction"]["Degrees"], + ATTR_FORECAST_CONDITION: [ + k for k, v in CONDITION_CLASSES.items() if item["IconDay"] in v + ][0], + } + for item in self.coordinator.data[ATTR_FORECAST] + ] + return forecast + + @staticmethod + def _calc_precipitation(day: dict) -> float: + """Return sum of the precipitation.""" + precip_sum = 0 + precip_types = ["Rain", "Snow", "Ice"] + for precip in precip_types: + precip_sum = sum( + [ + precip_sum, + day[f"{precip}Day"]["Value"], + day[f"{precip}Night"]["Value"], + ] + ) + return round(precip_sum, 1) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acer_projector/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/acer_projector/__init__.py new file mode 100644 index 00000000000..39896d203b1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acer_projector/__init__.py @@ -0,0 +1 @@ +"""The acer_projector component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acer_projector/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/acer_projector/const.py new file mode 100644 index 00000000000..98864ab957f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acer_projector/const.py @@ -0,0 +1,34 @@ +"""Use serial protocol of Acer projector to obtain state of the projector.""" +from __future__ import annotations + +from typing import Final + +from homeassistant.const import STATE_OFF, STATE_ON + +CONF_WRITE_TIMEOUT: Final = "write_timeout" + +DEFAULT_NAME: Final = "Acer Projector" +DEFAULT_TIMEOUT: Final = 1 +DEFAULT_WRITE_TIMEOUT: Final = 1 + +ECO_MODE: Final = "ECO Mode" + +ICON: Final = "mdi:projector" + +INPUT_SOURCE: Final = "Input Source" + +LAMP: Final = "Lamp" +LAMP_HOURS: Final = "Lamp Hours" + +MODEL: Final = "Model" + +# Commands known to the projector +CMD_DICT: Final[dict[str, str]] = { + LAMP: "* 0 Lamp ?\r", + LAMP_HOURS: "* 0 Lamp\r", + INPUT_SOURCE: "* 0 Src ?\r", + ECO_MODE: "* 0 IR 052\r", + MODEL: "* 0 IR 035\r", + STATE_ON: "* 0 IR 001\r", + STATE_OFF: "* 0 IR 002\r", +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acer_projector/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acer_projector/manifest.json new file mode 100644 index 00000000000..1120b5c93d0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acer_projector/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "acer_projector", + "name": "Acer Projector", + "documentation": "https://www.home-assistant.io/integrations/acer_projector", + "requirements": ["pyserial==3.5"], + "codeowners": [], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acer_projector/switch.py b/homeassistant-2021.6.0.dev0/homeassistant/components/acer_projector/switch.py new file mode 100644 index 00000000000..69aba415589 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acer_projector/switch.py @@ -0,0 +1,172 @@ +"""Use serial protocol of Acer projector to obtain state of the projector.""" +from __future__ import annotations + +import logging +import re +from typing import Any + +import serial +import voluptuous as vol + +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.const import ( + CONF_FILENAME, + CONF_NAME, + CONF_TIMEOUT, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from .const import ( + CMD_DICT, + CONF_WRITE_TIMEOUT, + DEFAULT_NAME, + DEFAULT_TIMEOUT, + DEFAULT_WRITE_TIMEOUT, + ECO_MODE, + ICON, + INPUT_SOURCE, + LAMP, + LAMP_HOURS, +) + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_FILENAME): cv.isdevice, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional( + CONF_WRITE_TIMEOUT, default=DEFAULT_WRITE_TIMEOUT + ): cv.positive_int, + } +) + + +def setup_platform( + hass: HomeAssistant, + config: ConfigType, + add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType, +) -> None: + """Connect with serial port and return Acer Projector.""" + serial_port = config[CONF_FILENAME] + name = config[CONF_NAME] + timeout = config[CONF_TIMEOUT] + write_timeout = config[CONF_WRITE_TIMEOUT] + + add_entities([AcerSwitch(serial_port, name, timeout, write_timeout)], True) + + +class AcerSwitch(SwitchEntity): + """Represents an Acer Projector as a switch.""" + + def __init__( + self, + serial_port: str, + name: str, + timeout: int, + write_timeout: int, + ) -> None: + """Init of the Acer projector.""" + self.ser = serial.Serial( + port=serial_port, timeout=timeout, write_timeout=write_timeout + ) + self._serial_port = serial_port + self._name = name + self._state = False + self._available = False + self._attributes = { + LAMP_HOURS: STATE_UNKNOWN, + INPUT_SOURCE: STATE_UNKNOWN, + ECO_MODE: STATE_UNKNOWN, + } + + def _write_read(self, msg: str) -> str: + """Write to the projector and read the return.""" + ret = "" + # Sometimes the projector won't answer for no reason or the projector + # was disconnected during runtime. + # This way the projector can be reconnected and will still work + try: + if not self.ser.is_open: + self.ser.open() + self.ser.write(msg.encode("utf-8")) + # Size is an experience value there is no real limit. + # AFAIK there is no limit and no end character so we will usually + # need to wait for timeout + ret = self.ser.read_until(size=20).decode("utf-8") + except serial.SerialException: + _LOGGER.error("Problem communicating with %s", self._serial_port) + self.ser.close() + return ret + + def _write_read_format(self, msg: str) -> str: + """Write msg, obtain answer and format output.""" + # answers are formatted as ***\answer\r*** + awns = self._write_read(msg) + match = re.search(r"\r(.+)\r", awns) + if match: + return match.group(1) + return STATE_UNKNOWN + + @property + def available(self) -> bool: + """Return if projector is available.""" + return self._available + + @property + def name(self) -> str: + """Return name of the projector.""" + return self._name + + @property + def icon(self) -> str: + """Return the icon.""" + return ICON + + @property + def is_on(self) -> bool: + """Return if the projector is turned on.""" + return self._state + + @property + def extra_state_attributes(self) -> dict[str, str]: + """Return state attributes.""" + return self._attributes + + def update(self) -> None: + """Get the latest state from the projector.""" + awns = self._write_read_format(CMD_DICT[LAMP]) + if awns == "Lamp 1": + self._state = True + self._available = True + elif awns == "Lamp 0": + self._state = False + self._available = True + else: + self._available = False + + for key in self._attributes: + msg = CMD_DICT.get(key) + if msg: + awns = self._write_read_format(msg) + self._attributes[key] = awns + + def turn_on(self, **kwargs: Any) -> None: + """Turn the projector on.""" + msg = CMD_DICT[STATE_ON] + self._write_read(msg) + self._state = True + + def turn_off(self, **kwargs: Any) -> None: + """Turn the projector off.""" + msg = CMD_DICT[STATE_OFF] + self._write_read(msg) + self._state = False diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/__init__.py new file mode 100644 index 00000000000..078c499f2be --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/__init__.py @@ -0,0 +1,46 @@ +"""The Rollease Acmeda Automate integration.""" + +from homeassistant import config_entries, core + +from .const import DOMAIN +from .hub import PulseHub + +CONF_HUBS = "hubs" + +PLATFORMS = ["cover", "sensor"] + + +async def async_setup_entry( + hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry +): + """Set up Rollease Acmeda Automate hub from a config entry.""" + hub = PulseHub(hass, config_entry) + + if not await hub.async_setup(): + return False + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][config_entry.entry_id] = hub + + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + + return True + + +async def async_unload_entry( + hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry +): + """Unload a config entry.""" + hub = hass.data[DOMAIN][config_entry.entry_id] + + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + + if not await hub.async_reset(): + return False + + if unload_ok: + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/base.py b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/base.py new file mode 100644 index 00000000000..15f9716db47 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/base.py @@ -0,0 +1,87 @@ +"""Base class for Acmeda Roller Blinds.""" +import aiopulse + +from homeassistant.core import callback +from homeassistant.helpers import entity +from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg + +from .const import ACMEDA_ENTITY_REMOVE, DOMAIN, LOGGER + + +class AcmedaBase(entity.Entity): + """Base representation of an Acmeda roller.""" + + def __init__(self, roller: aiopulse.Roller): + """Initialize the roller.""" + self.roller = roller + + async def async_remove_and_unregister(self): + """Unregister from entity and device registry and call entity remove function.""" + LOGGER.error("Removing %s %s", self.__class__.__name__, self.unique_id) + + ent_registry = await get_ent_reg(self.hass) + if self.entity_id in ent_registry.entities: + ent_registry.async_remove(self.entity_id) + + dev_registry = await get_dev_reg(self.hass) + device = dev_registry.async_get_device(identifiers={(DOMAIN, self.unique_id)}) + if device is not None: + dev_registry.async_update_device( + device.id, remove_config_entry_id=self.registry_entry.config_entry_id + ) + + await self.async_remove(force_remove=True) + + async def async_added_to_hass(self): + """Entity has been added to hass.""" + self.roller.callback_subscribe(self.notify_update) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + ACMEDA_ENTITY_REMOVE.format(self.roller.id), + self.async_remove_and_unregister, + ) + ) + + async def async_will_remove_from_hass(self): + """Entity being removed from hass.""" + self.roller.callback_unsubscribe(self.notify_update) + + @callback + def notify_update(self): + """Write updated device state information.""" + LOGGER.debug("Device update notification received: %s", self.name) + self.async_write_ha_state() + + @property + def should_poll(self): + """Report that Acmeda entities do not need polling.""" + return False + + @property + def unique_id(self): + """Return the unique ID of this roller.""" + return self.roller.id + + @property + def device_id(self): + """Return the ID of this roller.""" + return self.roller.id + + @property + def name(self): + """Return the name of roller.""" + return self.roller.name + + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.roller.name, + "manufacturer": "Rollease Acmeda", + "via_device": (DOMAIN, self.roller.hub.id), + } diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/config_flow.py new file mode 100644 index 00000000000..1f288e84bc7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/config_flow.py @@ -0,0 +1,68 @@ +"""Config flow for Rollease Acmeda Automate Pulse Hub.""" +from __future__ import annotations + +import asyncio +from contextlib import suppress + +import aiopulse +import async_timeout +import voluptuous as vol + +from homeassistant import config_entries + +from .const import DOMAIN + + +class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Acmeda config flow.""" + + VERSION = 1 + + def __init__(self): + """Initialize the config flow.""" + self.discovered_hubs: dict[str, aiopulse.Hub] | None = None + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if ( + user_input is not None + and self.discovered_hubs is not None + and user_input["id"] in self.discovered_hubs + ): + return await self.async_create(self.discovered_hubs[user_input["id"]]) + + # Already configured hosts + already_configured = { + entry.unique_id for entry in self._async_current_entries() + } + + hubs = [] + with suppress(asyncio.TimeoutError): + async with async_timeout.timeout(5): + async for hub in aiopulse.Hub.discover(): + if hub.id not in already_configured: + hubs.append(hub) + + if not hubs: + return self.async_abort(reason="no_devices_found") + + if len(hubs) == 1: + return await self.async_create(hubs[0]) + + self.discovered_hubs = {hub.id: hub for hub in hubs} + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required("id"): vol.In( + {hub.id: f"{hub.id} {hub.host}" for hub in hubs} + ) + } + ), + ) + + async def async_create(self, hub): + """Create the Acmeda Hub entry.""" + await self.async_set_unique_id(hub.id, raise_on_progress=False) + return self.async_create_entry(title=hub.id, data={"host": hub.host}) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/const.py new file mode 100644 index 00000000000..b8712fee4ba --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/const.py @@ -0,0 +1,8 @@ +"""Constants for the Rollease Acmeda Automate integration.""" +import logging + +LOGGER = logging.getLogger(__package__) +DOMAIN = "acmeda" + +ACMEDA_HUB_UPDATE = "acmeda_hub_update_{}" +ACMEDA_ENTITY_REMOVE = "acmeda_entity_remove_{}" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/cover.py b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/cover.py new file mode 100644 index 00000000000..82c61202cd3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/cover.py @@ -0,0 +1,121 @@ +"""Support for Acmeda Roller Blinds.""" +from homeassistant.components.cover import ( + ATTR_POSITION, + SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, + SUPPORT_OPEN, + SUPPORT_OPEN_TILT, + SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, + SUPPORT_STOP, + SUPPORT_STOP_TILT, + CoverEntity, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .base import AcmedaBase +from .const import ACMEDA_HUB_UPDATE, DOMAIN +from .helpers import async_add_acmeda_entities + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Acmeda Rollers from a config entry.""" + hub = hass.data[DOMAIN][config_entry.entry_id] + + current = set() + + @callback + def async_add_acmeda_covers(): + async_add_acmeda_entities( + hass, AcmedaCover, config_entry, current, async_add_entities + ) + + hub.cleanup_callbacks.append( + async_dispatcher_connect( + hass, + ACMEDA_HUB_UPDATE.format(config_entry.entry_id), + async_add_acmeda_covers, + ) + ) + + +class AcmedaCover(AcmedaBase, CoverEntity): + """Representation of a Acmeda cover device.""" + + @property + def current_cover_position(self): + """Return the current position of the roller blind. + + None is unknown, 0 is closed, 100 is fully open. + """ + position = None + if self.roller.type != 7: + position = 100 - self.roller.closed_percent + return position + + @property + def current_cover_tilt_position(self): + """Return the current tilt of the roller blind. + + None is unknown, 0 is closed, 100 is fully open. + """ + position = None + if self.roller.type in [7, 10]: + position = 100 - self.roller.closed_percent + return position + + @property + def supported_features(self): + """Flag supported features.""" + supported_features = 0 + if self.current_cover_position is not None: + supported_features |= ( + SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION + ) + if self.current_cover_tilt_position is not None: + supported_features |= ( + SUPPORT_OPEN_TILT + | SUPPORT_CLOSE_TILT + | SUPPORT_STOP_TILT + | SUPPORT_SET_TILT_POSITION + ) + + return supported_features + + @property + def is_closed(self): + """Return if the cover is closed.""" + return self.roller.closed_percent == 100 + + async def async_close_cover(self, **kwargs): + """Close the roller.""" + await self.roller.move_down() + + async def async_open_cover(self, **kwargs): + """Open the roller.""" + await self.roller.move_up() + + async def async_stop_cover(self, **kwargs): + """Stop the roller.""" + await self.roller.move_stop() + + async def async_set_cover_position(self, **kwargs): + """Move the roller shutter to a specific position.""" + await self.roller.move_to(100 - kwargs[ATTR_POSITION]) + + async def async_close_cover_tilt(self, **kwargs): + """Close the roller.""" + await self.roller.move_down() + + async def async_open_cover_tilt(self, **kwargs): + """Open the roller.""" + await self.roller.move_up() + + async def async_stop_cover_tilt(self, **kwargs): + """Stop the roller.""" + await self.roller.move_stop() + + async def async_set_cover_tilt(self, **kwargs): + """Tilt the roller shutter to a specific position.""" + await self.roller.move_to(100 - kwargs[ATTR_POSITION]) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/errors.py b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/errors.py new file mode 100644 index 00000000000..f26090df03d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/errors.py @@ -0,0 +1,10 @@ +"""Errors for the Acmeda Pulse component.""" +from homeassistant.exceptions import HomeAssistantError + + +class PulseException(HomeAssistantError): + """Base class for Acmeda Pulse exceptions.""" + + +class CannotConnect(PulseException): + """Unable to connect to the bridge.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/helpers.py b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/helpers.py new file mode 100644 index 00000000000..1162aba5dc8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/helpers.py @@ -0,0 +1,40 @@ +"""Helper functions for Acmeda Pulse.""" +from homeassistant.core import callback +from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg + +from .const import DOMAIN, LOGGER + + +@callback +def async_add_acmeda_entities( + hass, entity_class, config_entry, current, async_add_entities +): + """Add any new entities.""" + hub = hass.data[DOMAIN][config_entry.entry_id] + LOGGER.debug("Looking for new %s on: %s", entity_class.__name__, hub.host) + + api = hub.api.rollers + + new_items = [] + for unique_id, roller in api.items(): + if unique_id not in current: + LOGGER.debug("New %s %s", entity_class.__name__, unique_id) + new_item = entity_class(roller) + current.add(unique_id) + new_items.append(new_item) + + async_add_entities(new_items) + + +async def update_devices(hass, config_entry, api): + """Tell hass that device info has been updated.""" + dev_registry = await get_dev_reg(hass) + + for api_item in api.values(): + # Update Device name + device = dev_registry.async_get_device(identifiers={(DOMAIN, api_item.id)}) + if device is not None: + dev_registry.async_update_device( + device.id, + name=api_item.name, + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/hub.py b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/hub.py new file mode 100644 index 00000000000..e156ee5cb78 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/hub.py @@ -0,0 +1,89 @@ +"""Code to handle a Pulse Hub.""" +from __future__ import annotations + +import asyncio + +import aiopulse + +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import ACMEDA_ENTITY_REMOVE, ACMEDA_HUB_UPDATE, LOGGER +from .helpers import update_devices + + +class PulseHub: + """Manages a single Pulse Hub.""" + + def __init__(self, hass, config_entry): + """Initialize the system.""" + self.config_entry = config_entry + self.hass = hass + self.api: aiopulse.Hub | None = None + self.tasks = [] + self.current_rollers = {} + self.cleanup_callbacks = [] + + @property + def title(self): + """Return the title of the hub shown in the integrations list.""" + return f"{self.api.id} ({self.api.host})" + + @property + def host(self): + """Return the host of this hub.""" + return self.config_entry.data["host"] + + async def async_setup(self, tries=0): + """Set up a hub based on host parameter.""" + host = self.host + + hub = aiopulse.Hub(host) + self.api = hub + + hub.callback_subscribe(self.async_notify_update) + self.tasks.append(asyncio.create_task(hub.run())) + + LOGGER.debug("Hub setup complete") + return True + + async def async_reset(self): + """Reset this hub to default state.""" + + for cleanup_callback in self.cleanup_callbacks: + cleanup_callback() + + # If not setup + if self.api is None: + return False + + self.api.callback_unsubscribe(self.async_notify_update) + await self.api.stop() + del self.api + self.api = None + + # Wait for any running tasks to complete + await asyncio.wait(self.tasks) + + return True + + async def async_notify_update(self, update_type): + """Evaluate entities when hub reports that update has occurred.""" + LOGGER.debug("Hub {update_type.name} updated") + + if update_type == aiopulse.UpdateType.rollers: + await update_devices(self.hass, self.config_entry, self.api.rollers) + self.hass.config_entries.async_update_entry( + self.config_entry, title=self.title + ) + + async_dispatcher_send( + self.hass, ACMEDA_HUB_UPDATE.format(self.config_entry.entry_id) + ) + + for unique_id in list(self.current_rollers): + if unique_id not in self.api.rollers: + LOGGER.debug("Notifying remove of %s", unique_id) + self.current_rollers.pop(unique_id) + async_dispatcher_send( + self.hass, ACMEDA_ENTITY_REMOVE.format(unique_id) + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/manifest.json new file mode 100644 index 00000000000..ae72df5a323 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "acmeda", + "name": "Rollease Acmeda Automate", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/acmeda", + "requirements": ["aiopulse==0.4.2"], + "codeowners": ["@atmurray"], + "iot_class": "local_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/sensor.py new file mode 100644 index 00000000000..4f617c5726f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/sensor.py @@ -0,0 +1,47 @@ +"""Support for Acmeda Roller Blind Batteries.""" +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .base import AcmedaBase +from .const import ACMEDA_HUB_UPDATE, DOMAIN +from .helpers import async_add_acmeda_entities + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Acmeda Rollers from a config entry.""" + hub = hass.data[DOMAIN][config_entry.entry_id] + + current = set() + + @callback + def async_add_acmeda_sensors(): + async_add_acmeda_entities( + hass, AcmedaBattery, config_entry, current, async_add_entities + ) + + hub.cleanup_callbacks.append( + async_dispatcher_connect( + hass, + ACMEDA_HUB_UPDATE.format(config_entry.entry_id), + async_add_acmeda_sensors, + ) + ) + + +class AcmedaBattery(AcmedaBase, SensorEntity): + """Representation of a Acmeda cover device.""" + + device_class = DEVICE_CLASS_BATTERY + unit_of_measurement = PERCENTAGE + + @property + def name(self): + """Return the name of roller.""" + return f"{super().name} Battery" + + @property + def state(self): + """Return the state of the device.""" + return self.roller.battery diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/strings.json new file mode 100644 index 00000000000..f6c94581052 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/strings.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "user": { + "title": "Pick a hub to add", + "data": { + "id": "Host ID" + } + } + }, + "abort": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/ca.json new file mode 100644 index 00000000000..3c31dac301d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "step": { + "user": { + "data": { + "id": "ID d'amfitri\u00f3" + }, + "title": "Selecci\u00f3 del Hub a afegir" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/cs.json new file mode 100644 index 00000000000..3f392ed0347 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "step": { + "user": { + "data": { + "id": "ID hostitele" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/de.json new file mode 100644 index 00000000000..94834cde427 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/de.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "step": { + "user": { + "data": { + "id": "Host-ID" + }, + "title": "W\u00e4hle einen Hub zum Hinzuf\u00fcgen aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/en.json new file mode 100644 index 00000000000..1447785f078 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/en.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No devices found on the network" + }, + "step": { + "user": { + "data": { + "id": "Host ID" + }, + "title": "Pick a hub to add" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/es.json new file mode 100644 index 00000000000..6e336c0315b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/es.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se encontraron dispositivos en la red" + }, + "step": { + "user": { + "data": { + "id": "ID de host" + }, + "title": "Elige un hub para a\u00f1adir" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/et.json new file mode 100644 index 00000000000..2d1b37de6aa --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/et.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V\u00f5rgus ei tuvastatud \u00fchtegi seadet" + }, + "step": { + "user": { + "data": { + "id": "Hosti ID" + }, + "title": "Vali lisatav jaotur" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/fr.json new file mode 100644 index 00000000000..3ae9ff4234a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/fr.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "step": { + "user": { + "data": { + "id": "ID de l'h\u00f4te" + }, + "title": "Choisissez un hub \u00e0 ajouter" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/hu.json new file mode 100644 index 00000000000..6105977de80 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/id.json new file mode 100644 index 00000000000..6e80d134f5a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/id.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "step": { + "user": { + "data": { + "id": "ID Host" + }, + "title": "Pilih hub untuk ditambahkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/it.json new file mode 100644 index 00000000000..8592e6cc8da --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "step": { + "user": { + "data": { + "id": "ID host" + }, + "title": "Scegliere un hub da aggiungere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/ko.json new file mode 100644 index 00000000000..098d3a952f5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "id": "\ud638\uc2a4\ud2b8 ID" + }, + "title": "\ucd94\uac00\ud560 \ud5c8\ube0c \uc120\ud0dd\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/lb.json new file mode 100644 index 00000000000..8d5bfcf0edb --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/lb.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keng Apparater am Netzwierk fonnt" + }, + "step": { + "user": { + "data": { + "id": "Host ID" + }, + "title": "Wiel den Hub aus dee soll dob\u00e4igesat ginn." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/nl.json new file mode 100644 index 00000000000..aac926ec048 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Geen apparaten gevonden op het netwerk" + }, + "step": { + "user": { + "data": { + "id": "Host ID" + }, + "title": "Kies een hub om toe te voegen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/no.json new file mode 100644 index 00000000000..45d764e8112 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/no.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" + }, + "step": { + "user": { + "data": { + "id": "Vert ID" + }, + "title": "Velg en hub du vil legge til" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/pl.json new file mode 100644 index 00000000000..bdc14d83bfb --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "step": { + "user": { + "data": { + "id": "ID hosta" + }, + "title": "Wybierz hub, kt\u00f3ry chcesz doda\u0107" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/pt.json new file mode 100644 index 00000000000..8fcd9c13425 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/ru.json new file mode 100644 index 00000000000..14114706bd9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + }, + "step": { + "user": { + "data": { + "id": "ID \u0445\u043e\u0441\u0442\u0430" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0445\u0430\u0431, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0443\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/tr.json new file mode 100644 index 00000000000..aea81abdcba --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "id": "Ana bilgisayar kimli\u011fi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/uk.json new file mode 100644 index 00000000000..245428e9c73 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/uk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456." + }, + "step": { + "user": { + "data": { + "id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0445\u043e\u0441\u0442\u0430" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0445\u0430\u0431, \u044f\u043a\u0438\u0439 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0434\u043e\u0434\u0430\u0442\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/zh-Hant.json new file mode 100644 index 00000000000..2aeb94f66d2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/acmeda/translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "step": { + "user": { + "data": { + "id": "\u4e3b\u6a5f ID" + }, + "title": "\u9078\u64c7\u6240\u8981\u65b0\u589e\u7684 Hub" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/actiontec/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/actiontec/__init__.py new file mode 100644 index 00000000000..fa59cc87063 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/actiontec/__init__.py @@ -0,0 +1 @@ +"""The actiontec component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/actiontec/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/actiontec/const.py new file mode 100644 index 00000000000..1043bd1bdb6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/actiontec/const.py @@ -0,0 +1,12 @@ +"""Support for Actiontec MI424WR (Verizon FIOS) routers.""" +from __future__ import annotations + +import re +from typing import Final + +LEASES_REGEX: Final[re.Pattern] = re.compile( + r"(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})" + + r"\smac:\s(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))" + + r"\svalid\sfor:\s(?P(-?\d+))" + + r"\ssec" +) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/actiontec/device_tracker.py b/homeassistant-2021.6.0.dev0/homeassistant/components/actiontec/device_tracker.py new file mode 100644 index 00000000000..d7e6f5be494 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/actiontec/device_tracker.py @@ -0,0 +1,115 @@ +"""Support for Actiontec MI424WR (Verizon FIOS) routers.""" +from __future__ import annotations + +import logging +import telnetlib +from typing import Final + +import voluptuous as vol + +from homeassistant.components.device_tracker import ( + DOMAIN, + PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, + DeviceScanner, +) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType + +from .const import LEASES_REGEX +from .model import Device + +_LOGGER: Final = logging.getLogger(__name__) + +PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + } +) + + +def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: + """Validate the configuration and return an Actiontec scanner.""" + scanner = ActiontecDeviceScanner(config[DOMAIN]) + return scanner if scanner.success_init else None + + +class ActiontecDeviceScanner(DeviceScanner): + """This class queries an actiontec router for connected devices.""" + + def __init__(self, config: ConfigType) -> None: + """Initialize the scanner.""" + self.host: str = config[CONF_HOST] + self.username: str = config[CONF_USERNAME] + self.password: str = config[CONF_PASSWORD] + self.last_results: list[Device] = [] + data = self.get_actiontec_data() + self.success_init = data is not None + _LOGGER.info("Scanner initialized") + + def scan_devices(self) -> list[str]: + """Scan for new devices and return a list with found device IDs.""" + self._update_info() + return [client.mac_address for client in self.last_results] + + def get_device_name(self, device: str) -> str | None: # type: ignore[override] + """Return the name of the given device or None if we don't know.""" + for client in self.last_results: + if client.mac_address == device: + return client.ip_address + return None + + def _update_info(self) -> bool: + """Ensure the information from the router is up to date. + + Return boolean if scanning successful. + """ + _LOGGER.info("Scanning") + if not self.success_init: + return False + + actiontec_data = self.get_actiontec_data() + if actiontec_data is None: + return False + self.last_results = [ + device for device in actiontec_data if device.timevalid > -60 + ] + _LOGGER.info("Scan successful") + return True + + def get_actiontec_data(self) -> list[Device] | None: + """Retrieve data from Actiontec MI424WR and return parsed result.""" + try: + telnet = telnetlib.Telnet(self.host) + telnet.read_until(b"Username: ") + telnet.write((f"{self.username}\n").encode("ascii")) + telnet.read_until(b"Password: ") + telnet.write((f"{self.password}\n").encode("ascii")) + prompt = telnet.read_until(b"Wireless Broadband Router> ").split(b"\n")[-1] + telnet.write(b"firewall mac_cache_dump\n") + telnet.write(b"\n") + telnet.read_until(prompt) + leases_result = telnet.read_until(prompt).split(b"\n")[1:-1] + telnet.write(b"exit\n") + except EOFError: + _LOGGER.exception("Unexpected response from router") + return None + except ConnectionRefusedError: + _LOGGER.exception("Connection refused by router. Telnet enabled?") + return None + + devices: list[Device] = [] + for lease in leases_result: + match = LEASES_REGEX.search(lease.decode("utf-8")) + if match is not None: + devices.append( + Device( + match.group("ip"), + match.group("mac").upper(), + int(match.group("timevalid")), + ) + ) + return devices diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/actiontec/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/actiontec/manifest.json new file mode 100644 index 00000000000..a2573919629 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/actiontec/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "actiontec", + "name": "Actiontec", + "documentation": "https://www.home-assistant.io/integrations/actiontec", + "codeowners": [], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/actiontec/model.py b/homeassistant-2021.6.0.dev0/homeassistant/components/actiontec/model.py new file mode 100644 index 00000000000..ff28d6d4ac6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/actiontec/model.py @@ -0,0 +1,11 @@ +"""Model definitions for Actiontec MI424WR (Verizon FIOS) routers.""" +from dataclasses import dataclass + + +@dataclass +class Device: + """Actiontec device class.""" + + ip_address: str + mac_address: str + timevalid: int diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/__init__.py new file mode 100644 index 00000000000..0a4a79b65f5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/__init__.py @@ -0,0 +1,208 @@ +"""Support for AdGuard Home.""" +from __future__ import annotations + +import logging + +from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError +import voluptuous as vol + +from homeassistant.components.adguard.const import ( + CONF_FORCE, + DATA_ADGUARD_CLIENT, + DATA_ADGUARD_VERSION, + DOMAIN, + SERVICE_ADD_URL, + SERVICE_DISABLE_URL, + SERVICE_ENABLE_URL, + SERVICE_REFRESH, + SERVICE_REMOVE_URL, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_URL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +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.entity import DeviceInfo, Entity + +_LOGGER = logging.getLogger(__name__) + +SERVICE_URL_SCHEMA = vol.Schema({vol.Required(CONF_URL): cv.url}) +SERVICE_ADD_URL_SCHEMA = vol.Schema( + {vol.Required(CONF_NAME): cv.string, vol.Required(CONF_URL): cv.url} +) +SERVICE_REFRESH_SCHEMA = vol.Schema( + {vol.Optional(CONF_FORCE, default=False): cv.boolean} +) + +PLATFORMS = ["sensor", "switch"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up AdGuard Home from a config entry.""" + session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL]) + adguard = AdGuardHome( + entry.data[CONF_HOST], + port=entry.data[CONF_PORT], + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], + tls=entry.data[CONF_SSL], + verify_ssl=entry.data[CONF_VERIFY_SSL], + session=session, + ) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_ADGUARD_CLIENT: adguard} + + try: + await adguard.version() + except AdGuardHomeConnectionError as exception: + raise ConfigEntryNotReady from exception + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + async def add_url(call) -> None: + """Service call to add a new filter subscription to AdGuard Home.""" + await adguard.filtering.add_url( + allowlist=False, name=call.data.get(CONF_NAME), url=call.data.get(CONF_URL) + ) + + async def remove_url(call) -> None: + """Service call to remove a filter subscription from AdGuard Home.""" + await adguard.filtering.remove_url(allowlist=False, url=call.data.get(CONF_URL)) + + async def enable_url(call) -> None: + """Service call to enable a filter subscription in AdGuard Home.""" + await adguard.filtering.enable_url(allowlist=False, url=call.data.get(CONF_URL)) + + async def disable_url(call) -> None: + """Service call to disable a filter subscription in AdGuard Home.""" + await adguard.filtering.disable_url( + allowlist=False, url=call.data.get(CONF_URL) + ) + + async def refresh(call) -> None: + """Service call to refresh the filter subscriptions in AdGuard Home.""" + await adguard.filtering.refresh( + allowlist=False, force=call.data.get(CONF_FORCE) + ) + + hass.services.async_register( + DOMAIN, SERVICE_ADD_URL, add_url, schema=SERVICE_ADD_URL_SCHEMA + ) + hass.services.async_register( + DOMAIN, SERVICE_REMOVE_URL, remove_url, schema=SERVICE_URL_SCHEMA + ) + hass.services.async_register( + DOMAIN, SERVICE_ENABLE_URL, enable_url, schema=SERVICE_URL_SCHEMA + ) + hass.services.async_register( + DOMAIN, SERVICE_DISABLE_URL, disable_url, schema=SERVICE_URL_SCHEMA + ) + hass.services.async_register( + DOMAIN, SERVICE_REFRESH, refresh, schema=SERVICE_REFRESH_SCHEMA + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload AdGuard Home config entry.""" + hass.services.async_remove(DOMAIN, SERVICE_ADD_URL) + hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL) + hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL) + hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL) + hass.services.async_remove(DOMAIN, SERVICE_REFRESH) + + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + del hass.data[DOMAIN] + + return unload_ok + + +class AdGuardHomeEntity(Entity): + """Defines a base AdGuard Home entity.""" + + def __init__( + self, + adguard: AdGuardHome, + entry: ConfigEntry, + name: str, + icon: str, + enabled_default: bool = True, + ) -> None: + """Initialize the AdGuard Home entity.""" + self._available = True + self._enabled_default = enabled_default + self._icon = icon + self._name = name + self._entry = entry + self.adguard = adguard + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def icon(self) -> str: + """Return the mdi icon of the entity.""" + return self._icon + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self._enabled_default + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + async def async_update(self) -> None: + """Update AdGuard Home entity.""" + if not self.enabled: + return + + try: + await self._adguard_update() + self._available = True + except AdGuardHomeError: + if self._available: + _LOGGER.debug( + "An error occurred while updating AdGuard Home sensor", + exc_info=True, + ) + self._available = False + + async def _adguard_update(self) -> None: + """Update AdGuard Home entity.""" + raise NotImplementedError() + + +class AdGuardHomeDeviceEntity(AdGuardHomeEntity): + """Defines a AdGuard Home device entity.""" + + @property + def device_info(self) -> DeviceInfo: + """Return device information about this AdGuard Home instance.""" + return { + "identifiers": { + (DOMAIN, self.adguard.host, self.adguard.port, self.adguard.base_path) + }, + "name": "AdGuard Home", + "manufacturer": "AdGuard Team", + "sw_version": self.hass.data[DOMAIN][self._entry.entry_id].get( + DATA_ADGUARD_VERSION + ), + "entry_type": "service", + } diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/config_flow.py new file mode 100644 index 00000000000..bbb6d34954b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/config_flow.py @@ -0,0 +1,148 @@ +"""Config flow to configure the AdGuard Home integration.""" +from __future__ import annotations + +from typing import Any + +from adguardhome import AdGuardHome, AdGuardHomeConnectionError +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + + +class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): + """Handle a AdGuard Home config flow.""" + + VERSION = 1 + + _hassio_discovery = None + + async def _show_setup_form( + self, errors: dict[str, str] | None = None + ) -> FlowResult: + """Show the setup form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=3000): vol.Coerce(int), + vol.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, + vol.Required(CONF_SSL, default=True): bool, + vol.Required(CONF_VERIFY_SSL, default=True): bool, + } + ), + errors=errors or {}, + ) + + async def _show_hassio_form( + self, errors: dict[str, str] | None = None + ) -> FlowResult: + """Show the Hass.io confirmation form to the user.""" + return self.async_show_form( + step_id="hassio_confirm", + description_placeholders={"addon": self._hassio_discovery["addon"]}, + data_schema=vol.Schema({}), + errors=errors or {}, + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initiated by the user.""" + if user_input is None: + return await self._show_setup_form(user_input) + + self._async_abort_entries_match( + {CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]} + ) + + errors = {} + + session = async_get_clientsession(self.hass, user_input[CONF_VERIFY_SSL]) + + adguard = AdGuardHome( + user_input[CONF_HOST], + port=user_input[CONF_PORT], + username=user_input.get(CONF_USERNAME), + password=user_input.get(CONF_PASSWORD), + tls=user_input[CONF_SSL], + verify_ssl=user_input[CONF_VERIFY_SSL], + session=session, + ) + + try: + await adguard.version() + except AdGuardHomeConnectionError: + errors["base"] = "cannot_connect" + return await self._show_setup_form(errors) + + return self.async_create_entry( + title=user_input[CONF_HOST], + data={ + CONF_HOST: user_input[CONF_HOST], + CONF_PASSWORD: user_input.get(CONF_PASSWORD), + CONF_PORT: user_input[CONF_PORT], + CONF_SSL: user_input[CONF_SSL], + CONF_USERNAME: user_input.get(CONF_USERNAME), + CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL], + }, + ) + + async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResult: + """Prepare configuration for a Hass.io AdGuard Home add-on. + + This flow is triggered by the discovery component. + """ + await self._async_handle_discovery_without_unique_id() + + self._hassio_discovery = discovery_info + return await self.async_step_hassio_confirm() + + async def async_step_hassio_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm Supervisor discovery.""" + if user_input is None: + return await self._show_hassio_form() + + errors = {} + + session = async_get_clientsession(self.hass, False) + + adguard = AdGuardHome( + self._hassio_discovery[CONF_HOST], + port=self._hassio_discovery[CONF_PORT], + tls=False, + session=session, + ) + + try: + await adguard.version() + except AdGuardHomeConnectionError: + errors["base"] = "cannot_connect" + return await self._show_hassio_form(errors) + + return self.async_create_entry( + title=self._hassio_discovery["addon"], + data={ + CONF_HOST: self._hassio_discovery[CONF_HOST], + CONF_PORT: self._hassio_discovery[CONF_PORT], + CONF_PASSWORD: None, + CONF_SSL: False, + CONF_USERNAME: None, + CONF_VERIFY_SSL: True, + }, + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/const.py new file mode 100644 index 00000000000..8bfa5b49fc6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/const.py @@ -0,0 +1,14 @@ +"""Constants for the AdGuard Home integration.""" + +DOMAIN = "adguard" + +DATA_ADGUARD_CLIENT = "adguard_client" +DATA_ADGUARD_VERSION = "adguard_version" + +CONF_FORCE = "force" + +SERVICE_ADD_URL = "add_url" +SERVICE_DISABLE_URL = "disable_url" +SERVICE_ENABLE_URL = "enable_url" +SERVICE_REFRESH = "refresh" +SERVICE_REMOVE_URL = "remove_url" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/manifest.json new file mode 100644 index 00000000000..bd311dd3d35 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "adguard", + "name": "AdGuard Home", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/adguard", + "requirements": ["adguardhome==0.5.0"], + "codeowners": ["@frenck"], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/sensor.py new file mode 100644 index 00000000000..7499cf51d0c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/sensor.py @@ -0,0 +1,248 @@ +"""Support for AdGuard Home sensors.""" +from __future__ import annotations + +from datetime import timedelta + +from adguardhome import AdGuardHome, AdGuardHomeConnectionError + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, TIME_MILLISECONDS +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import AdGuardHomeDeviceEntity +from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN + +SCAN_INTERVAL = timedelta(seconds=300) +PARALLEL_UPDATES = 4 + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up AdGuard Home sensor based on a config entry.""" + adguard = hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_CLIENT] + + try: + version = await adguard.version() + except AdGuardHomeConnectionError as exception: + raise PlatformNotReady from exception + + hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_VERSION] = version + + sensors = [ + AdGuardHomeDNSQueriesSensor(adguard, entry), + AdGuardHomeBlockedFilteringSensor(adguard, entry), + AdGuardHomePercentageBlockedSensor(adguard, entry), + AdGuardHomeReplacedParentalSensor(adguard, entry), + AdGuardHomeReplacedSafeBrowsingSensor(adguard, entry), + AdGuardHomeReplacedSafeSearchSensor(adguard, entry), + AdGuardHomeAverageProcessingTimeSensor(adguard, entry), + AdGuardHomeRulesCountSensor(adguard, entry), + ] + + async_add_entities(sensors, True) + + +class AdGuardHomeSensor(AdGuardHomeDeviceEntity, SensorEntity): + """Defines a AdGuard Home sensor.""" + + def __init__( + self, + adguard: AdGuardHome, + entry: ConfigEntry, + name: str, + icon: str, + measurement: str, + unit_of_measurement: str, + enabled_default: bool = True, + ) -> None: + """Initialize AdGuard Home sensor.""" + self._state = None + self._unit_of_measurement = unit_of_measurement + self.measurement = measurement + + super().__init__(adguard, entry, name, icon, enabled_default) + + @property + def unique_id(self) -> str: + """Return the unique ID for this sensor.""" + return "_".join( + [ + DOMAIN, + self.adguard.host, + str(self.adguard.port), + "sensor", + self.measurement, + ] + ) + + @property + def state(self) -> str | None: + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self) -> str | None: + """Return the unit this state is expressed in.""" + return self._unit_of_measurement + + +class AdGuardHomeDNSQueriesSensor(AdGuardHomeSensor): + """Defines a AdGuard Home DNS Queries sensor.""" + + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: + """Initialize AdGuard Home sensor.""" + super().__init__( + adguard, + entry, + "AdGuard DNS Queries", + "mdi:magnify", + "dns_queries", + "queries", + ) + + async def _adguard_update(self) -> None: + """Update AdGuard Home entity.""" + self._state = await self.adguard.stats.dns_queries() + + +class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor): + """Defines a AdGuard Home blocked by filtering sensor.""" + + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: + """Initialize AdGuard Home sensor.""" + super().__init__( + adguard, + entry, + "AdGuard DNS Queries Blocked", + "mdi:magnify-close", + "blocked_filtering", + "queries", + enabled_default=False, + ) + + async def _adguard_update(self) -> None: + """Update AdGuard Home entity.""" + self._state = await self.adguard.stats.blocked_filtering() + + +class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor): + """Defines a AdGuard Home blocked percentage sensor.""" + + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: + """Initialize AdGuard Home sensor.""" + super().__init__( + adguard, + entry, + "AdGuard DNS Queries Blocked Ratio", + "mdi:magnify-close", + "blocked_percentage", + PERCENTAGE, + ) + + async def _adguard_update(self) -> None: + """Update AdGuard Home entity.""" + percentage = await self.adguard.stats.blocked_percentage() + self._state = f"{percentage:.2f}" + + +class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor): + """Defines a AdGuard Home replaced by parental control sensor.""" + + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: + """Initialize AdGuard Home sensor.""" + super().__init__( + adguard, + entry, + "AdGuard Parental Control Blocked", + "mdi:human-male-girl", + "blocked_parental", + "requests", + ) + + async def _adguard_update(self) -> None: + """Update AdGuard Home entity.""" + self._state = await self.adguard.stats.replaced_parental() + + +class AdGuardHomeReplacedSafeBrowsingSensor(AdGuardHomeSensor): + """Defines a AdGuard Home replaced by safe browsing sensor.""" + + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: + """Initialize AdGuard Home sensor.""" + super().__init__( + adguard, + entry, + "AdGuard Safe Browsing Blocked", + "mdi:shield-half-full", + "blocked_safebrowsing", + "requests", + ) + + async def _adguard_update(self) -> None: + """Update AdGuard Home entity.""" + self._state = await self.adguard.stats.replaced_safebrowsing() + + +class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor): + """Defines a AdGuard Home replaced by safe search sensor.""" + + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: + """Initialize AdGuard Home sensor.""" + super().__init__( + adguard, + entry, + "AdGuard Safe Searches Enforced", + "mdi:shield-search", + "enforced_safesearch", + "requests", + ) + + async def _adguard_update(self) -> None: + """Update AdGuard Home entity.""" + self._state = await self.adguard.stats.replaced_safesearch() + + +class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor): + """Defines a AdGuard Home average processing time sensor.""" + + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: + """Initialize AdGuard Home sensor.""" + super().__init__( + adguard, + entry, + "AdGuard Average Processing Speed", + "mdi:speedometer", + "average_speed", + TIME_MILLISECONDS, + ) + + async def _adguard_update(self) -> None: + """Update AdGuard Home entity.""" + average = await self.adguard.stats.avg_processing_time() + self._state = f"{average:.2f}" + + +class AdGuardHomeRulesCountSensor(AdGuardHomeSensor): + """Defines a AdGuard Home rules count sensor.""" + + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: + """Initialize AdGuard Home sensor.""" + super().__init__( + adguard, + entry, + "AdGuard Rules Count", + "mdi:counter", + "rules_count", + "rules", + enabled_default=False, + ) + + async def _adguard_update(self) -> None: + """Update AdGuard Home entity.""" + self._state = await self.adguard.filtering.rules_count(allowlist=False) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/services.yaml b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/services.yaml new file mode 100644 index 00000000000..2e97d164e3a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/services.yaml @@ -0,0 +1,66 @@ +add_url: + name: Add url + description: Add a new filter subscription to AdGuard Home. + fields: + name: + name: Name + description: The name of the filter subscription. + required: true + example: Example + selector: + text: + url: + name: Url + description: The filter URL to subscribe to, containing the filter rules. + required: true + example: https://www.example.com/filter/1.txt + selector: + text: + +remove_url: + name: Remove url + description: Removes a filter subscription from AdGuard Home. + fields: + url: + name: Url + description: The filter subscription URL to remove. + required: true + example: https://www.example.com/filter/1.txt + selector: + text: + +enable_url: + name: Enable url + description: Enables a filter subscription in AdGuard Home. + fields: + url: + name: Url + description: The filter subscription URL to enable. + required: true + example: https://www.example.com/filter/1.txt + selector: + text: + +disable_url: + name: Disable url + description: Disables a filter subscription in AdGuard Home. + fields: + url: + name: Url + description: The filter subscription URL to disable. + required: true + example: https://www.example.com/filter/1.txt + selector: + text: + +refresh: + name: Refresh + description: Refresh all filter subscriptions in AdGuard Home. + fields: + force: + name: Force + description: Force update (by passes AdGuard Home throttling). + example: '"true" to force, "false" or omit for a regular refresh.' + default: false + selector: + boolean: diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/strings.json new file mode 100644 index 00000000000..e593d4199a4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/strings.json @@ -0,0 +1,28 @@ +{ + "config": { + "step": { + "user": { + "description": "Set up your AdGuard Home instance to allow monitoring and control.", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "password": "[%key:common::config_flow::data::password%]", + "port": "[%key:common::config_flow::data::port%]", + "username": "[%key:common::config_flow::data::username%]", + "ssl": "[%key:common::config_flow::data::ssl%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + } + }, + "hassio_confirm": { + "title": "AdGuard Home via Home Assistant add-on", + "description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the add-on: {addon}?" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "existing_instance_updated": "Updated existing configuration.", + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/switch.py b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/switch.py new file mode 100644 index 00000000000..f6c41e7f2e8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/switch.py @@ -0,0 +1,238 @@ +"""Support for AdGuard Home switches.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import AdGuardHomeDeviceEntity +from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=10) +PARALLEL_UPDATES = 1 + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up AdGuard Home switch based on a config entry.""" + adguard = hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_CLIENT] + + try: + version = await adguard.version() + except AdGuardHomeConnectionError as exception: + raise PlatformNotReady from exception + + hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_VERSION] = version + + switches = [ + AdGuardHomeProtectionSwitch(adguard, entry), + AdGuardHomeFilteringSwitch(adguard, entry), + AdGuardHomeParentalSwitch(adguard, entry), + AdGuardHomeSafeBrowsingSwitch(adguard, entry), + AdGuardHomeSafeSearchSwitch(adguard, entry), + AdGuardHomeQueryLogSwitch(adguard, entry), + ] + async_add_entities(switches, True) + + +class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity): + """Defines a AdGuard Home switch.""" + + def __init__( + self, + adguard: AdGuardHome, + entry: ConfigEntry, + name: str, + icon: str, + key: str, + enabled_default: bool = True, + ) -> None: + """Initialize AdGuard Home switch.""" + self._state = False + self._key = key + super().__init__(adguard, entry, name, icon, enabled_default) + + @property + def unique_id(self) -> str: + """Return the unique ID for this sensor.""" + return "_".join( + [DOMAIN, self.adguard.host, str(self.adguard.port), "switch", self._key] + ) + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return self._state + + async def async_turn_off(self, **kwargs) -> None: + """Turn off the switch.""" + try: + await self._adguard_turn_off() + except AdGuardHomeError: + _LOGGER.error("An error occurred while turning off AdGuard Home switch") + self._available = False + + async def _adguard_turn_off(self) -> None: + """Turn off the switch.""" + raise NotImplementedError() + + async def async_turn_on(self, **kwargs) -> None: + """Turn on the switch.""" + try: + await self._adguard_turn_on() + except AdGuardHomeError: + _LOGGER.error("An error occurred while turning on AdGuard Home switch") + self._available = False + + async def _adguard_turn_on(self) -> None: + """Turn on the switch.""" + raise NotImplementedError() + + +class AdGuardHomeProtectionSwitch(AdGuardHomeSwitch): + """Defines a AdGuard Home protection switch.""" + + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: + """Initialize AdGuard Home switch.""" + super().__init__( + adguard, entry, "AdGuard Protection", "mdi:shield-check", "protection" + ) + + async def _adguard_turn_off(self) -> None: + """Turn off the switch.""" + await self.adguard.disable_protection() + + async def _adguard_turn_on(self) -> None: + """Turn on the switch.""" + await self.adguard.enable_protection() + + async def _adguard_update(self) -> None: + """Update AdGuard Home entity.""" + self._state = await self.adguard.protection_enabled() + + +class AdGuardHomeParentalSwitch(AdGuardHomeSwitch): + """Defines a AdGuard Home parental control switch.""" + + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: + """Initialize AdGuard Home switch.""" + super().__init__( + adguard, entry, "AdGuard Parental Control", "mdi:shield-check", "parental" + ) + + async def _adguard_turn_off(self) -> None: + """Turn off the switch.""" + await self.adguard.parental.disable() + + async def _adguard_turn_on(self) -> None: + """Turn on the switch.""" + await self.adguard.parental.enable() + + async def _adguard_update(self) -> None: + """Update AdGuard Home entity.""" + self._state = await self.adguard.parental.enabled() + + +class AdGuardHomeSafeSearchSwitch(AdGuardHomeSwitch): + """Defines a AdGuard Home safe search switch.""" + + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: + """Initialize AdGuard Home switch.""" + super().__init__( + adguard, entry, "AdGuard Safe Search", "mdi:shield-check", "safesearch" + ) + + async def _adguard_turn_off(self) -> None: + """Turn off the switch.""" + await self.adguard.safesearch.disable() + + async def _adguard_turn_on(self) -> None: + """Turn on the switch.""" + await self.adguard.safesearch.enable() + + async def _adguard_update(self) -> None: + """Update AdGuard Home entity.""" + self._state = await self.adguard.safesearch.enabled() + + +class AdGuardHomeSafeBrowsingSwitch(AdGuardHomeSwitch): + """Defines a AdGuard Home safe search switch.""" + + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: + """Initialize AdGuard Home switch.""" + super().__init__( + adguard, entry, "AdGuard Safe Browsing", "mdi:shield-check", "safebrowsing" + ) + + async def _adguard_turn_off(self) -> None: + """Turn off the switch.""" + await self.adguard.safebrowsing.disable() + + async def _adguard_turn_on(self) -> None: + """Turn on the switch.""" + await self.adguard.safebrowsing.enable() + + async def _adguard_update(self) -> None: + """Update AdGuard Home entity.""" + self._state = await self.adguard.safebrowsing.enabled() + + +class AdGuardHomeFilteringSwitch(AdGuardHomeSwitch): + """Defines a AdGuard Home filtering switch.""" + + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: + """Initialize AdGuard Home switch.""" + super().__init__( + adguard, entry, "AdGuard Filtering", "mdi:shield-check", "filtering" + ) + + async def _adguard_turn_off(self) -> None: + """Turn off the switch.""" + await self.adguard.filtering.disable() + + async def _adguard_turn_on(self) -> None: + """Turn on the switch.""" + await self.adguard.filtering.enable() + + async def _adguard_update(self) -> None: + """Update AdGuard Home entity.""" + self._state = await self.adguard.filtering.enabled() + + +class AdGuardHomeQueryLogSwitch(AdGuardHomeSwitch): + """Defines a AdGuard Home query log switch.""" + + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: + """Initialize AdGuard Home switch.""" + super().__init__( + adguard, + entry, + "AdGuard Query Log", + "mdi:shield-check", + "querylog", + enabled_default=False, + ) + + async def _adguard_turn_off(self) -> None: + """Turn off the switch.""" + await self.adguard.querylog.disable() + + async def _adguard_turn_on(self) -> None: + """Turn on the switch.""" + await self.adguard.querylog.enable() + + async def _adguard_update(self) -> None: + """Update AdGuard Home entity.""" + self._state = await self.adguard.querylog.enabled() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/bg.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/bg.json new file mode 100644 index 00000000000..97d8547861f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "existing_instance_updated": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "step": { + "hassio_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Home Assistant \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0437\u0432\u0430 \u0441 AdGuard Home, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d \u043e\u0442 Supervisor \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430: {addon} ?", + "title": "AdGuard Home \u0447\u0440\u0435\u0437 Supervisor \u0434\u043e\u0431\u0430\u0432\u043a\u0430" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "ssl": "AdGuard Home \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", + "verify_ssl": "AdGuard Home \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043d\u0430\u0434\u0435\u0436\u0434\u0435\u043d \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0412\u0430\u0448\u0438\u044f AdGuard Home, \u0437\u0430 \u0434\u0430 \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0435 \u043d\u0430\u0431\u043b\u044e\u0434\u0435\u043d\u0438\u0435 \u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/ca.json new file mode 100644 index 00000000000..300c843b57e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/ca.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat", + "existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "hassio_confirm": { + "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb l'AdGuard Home proporcionat pel complement: {addon}?", + "title": "AdGuard Home via complement de Home Assistant" + }, + "user": { + "data": { + "host": "[%key::common::config_flow::data::host%]", + "password": "[%key::common::config_flow::data::password%]", + "port": "[%key::common::config_flow::data::port%]", + "ssl": "Utilitza un certificat SSL", + "username": "[%key::common::config_flow::data::username%]", + "verify_ssl": "Verifica el certificat SSL" + }, + "description": "Configuraci\u00f3 de la inst\u00e0ncia d'AdGuard Home, permet el control i la monitoritzaci\u00f3." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/cs.json new file mode 100644 index 00000000000..f82589900d4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/cs.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena", + "existing_instance_updated": "St\u00e1vaj\u00edc\u00ed nastaven\u00ed aktualizov\u00e1no." + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "hassio_confirm": { + "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k AddGuard pomoc\u00ed Supervisor {addon}?", + "title": "AdGuard prost\u0159ednictv\u00edm dopl\u0148ku Supervisor" + }, + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "ssl": "Pou\u017e\u00edv\u00e1 SSL certifik\u00e1t", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no", + "verify_ssl": "Ov\u011b\u0159it certifik\u00e1t SSL" + }, + "description": "Nastavte svou instanci AdGuard Home pro monitorov\u00e1n\u00ed a \u0159\u00edzen\u00ed." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/da.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/da.json new file mode 100644 index 00000000000..8bb4c26eed6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/da.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "existing_instance_updated": "Opdaterede eksisterende konfiguration." + }, + "step": { + "hassio_confirm": { + "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til AdGuard Home leveret af Supervisor-tilf\u00f8jelsen: {addon}?", + "title": "AdGuard Home via Supervisor-tilf\u00f8jelse" + }, + "user": { + "data": { + "password": "Adgangskode", + "ssl": "AdGuard Home bruger et SSL-certifikat", + "username": "Brugernavn", + "verify_ssl": "AdGuard Home bruger et korrekt certifikat" + }, + "description": "Konfigurer din AdGuard Home-instans for at tillade overv\u00e5gning og kontrol." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/de.json new file mode 100644 index 00000000000..f73c25d769e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/de.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", + "existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "hassio_confirm": { + "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass eine Verbindung mit AdGuard Home als Supervisor-Add-On hergestellt wird: {addon}?", + "title": "AdGuard Home \u00fcber das Supervisor Add-on" + }, + "user": { + "data": { + "host": "Host", + "password": "Passwort", + "port": "Port", + "ssl": "AdGuard Home verwendet ein SSL-Zertifikat", + "username": "Benutzername", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + }, + "description": "Richte deine AdGuard Home-Instanz ein um sie zu \u00dcberwachen und zu Steuern." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/el.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/el.json new file mode 100644 index 00000000000..04b238a916d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/el.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/en.json new file mode 100644 index 00000000000..f354aaab10a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/en.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "existing_instance_updated": "Updated existing configuration." + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "hassio_confirm": { + "description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the add-on: {addon}?", + "title": "AdGuard Home via Home Assistant add-on" + }, + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Port", + "ssl": "Uses an SSL certificate", + "username": "Username", + "verify_ssl": "Verify SSL certificate" + }, + "description": "Set up your AdGuard Home instance to allow monitoring and control." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/es-419.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/es-419.json new file mode 100644 index 00000000000..6a734ffea9a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/es-419.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "existing_instance_updated": "Se actualiz\u00f3 la configuraci\u00f3n existente." + }, + "step": { + "hassio_confirm": { + "description": "\u00bfDesea configurar Home Assistant para conectarse a la p\u00e1gina principal de AdGuard proporcionada por el complemento Supervisor: {addon}?", + "title": "AdGuard Home a trav\u00e9s del complemento Supervisor" + }, + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "port": "Puerto", + "ssl": "AdGuard Home utiliza un certificado SSL", + "username": "Nombre de usuario", + "verify_ssl": "AdGuard Home utiliza un certificado adecuado" + }, + "description": "Configure su instancia de AdGuard Home para permitir la supervisi\u00f3n y el control." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/es.json new file mode 100644 index 00000000000..5750808ab76 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/es.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado", + "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente." + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "hassio_confirm": { + "description": "\u00bfDesea configurar Home Assistant para conectarse al AdGuard Home proporcionado por el complemento Supervisor: {addon} ?", + "title": "AdGuard Home a trav\u00e9s del complemento Supervisor" + }, + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "port": "Puerto", + "ssl": "AdGuard Home utiliza un certificado SSL", + "username": "Usuario", + "verify_ssl": "AdGuard Home utiliza un certificado apropiado" + }, + "description": "Configure su instancia de AdGuard Home para permitir la supervisi\u00f3n y el control." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/et.json new file mode 100644 index 00000000000..fc1d043994e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/et.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud", + "existing_instance_updated": "Olemasolevad seaded v\u00e4rskendatud." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "hassio_confirm": { + "description": "Kas soovid seadistada Home Assistant-i \u00fchenduse AdGuard Home'iga mida pakub lisandmoodul: {addon} ?", + "title": "AdGuard Home Home Assistanti lisandmooduli abil" + }, + "user": { + "data": { + "host": "", + "password": "Salas\u00f5na", + "port": "", + "ssl": "Kasuta SSL sertifikaati", + "username": "Kasutajanimi", + "verify_ssl": "Kontrolli SSL sertifikaati" + }, + "description": "Seadista AdGuard Home'i sidumine, et v\u00f5imaldada j\u00e4lgimist ja juhtimist." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/fi.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/fi.json new file mode 100644 index 00000000000..fe41954e79b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/fi.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Palvelin", + "port": "Portti" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/fr.json new file mode 100644 index 00000000000..7add7c9829f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/fr.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour." + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "hassio_confirm": { + "description": "Voulez-vous configurer Home Assistant pour qu'il se connecte \u00e0 AdGuard Home fourni par le module compl\u00e9mentaire Hass.io: {addon} ?", + "title": "AdGuard Home via le module compl\u00e9mentaire Hass.io" + }, + "user": { + "data": { + "host": "H\u00f4te", + "password": "Mot de passe", + "port": "Port", + "ssl": "AdGuard Home utilise un certificat SSL", + "username": "Nom d'utilisateur", + "verify_ssl": "AdGuard Home utilise un certificat appropri\u00e9" + }, + "description": "Configurez votre instance AdGuard Home pour permettre la surveillance et le contr\u00f4le." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/he.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/he.json new file mode 100644 index 00000000000..1471fd6603b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/he.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Host", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05d5\u05e8\u05d8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/hr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/hr.json new file mode 100644 index 00000000000..869cc46ea10 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/hr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "existing_instance_updated": "Postoje\u0107a konfiguracija je a\u017eurirana." + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/hu.json new file mode 100644 index 00000000000..251b72574ee --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "Hoszt", + "password": "Jelsz\u00f3", + "port": "Port", + "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/id.json new file mode 100644 index 00000000000..d787fd5620d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/id.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "existing_instance_updated": "Memperbarui konfigurasi yang ada." + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "hassio_confirm": { + "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke AdGuard Home yang disediakan oleh add-on Supervisor {addon}?", + "title": "AdGuard Home melalui add-on Home Assistant" + }, + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "port": "Port", + "ssl": "Menggunakan sertifikat SSL", + "username": "Nama Pengguna", + "verify_ssl": "Verifikasi sertifikat SSL" + }, + "description": "Siapkan instans AdGuard Home Anda untuk pemantauan dan kontrol." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/it.json new file mode 100644 index 00000000000..5f8bc33997d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/it.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "existing_instance_updated": "Configurazione esistente aggiornata." + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "hassio_confirm": { + "description": "Vuoi configurare Home Assistant per connettersi ad AdGuard Home fornito dal componente aggiuntivo: {addon}?", + "title": "AdGuard Home tramite il componente aggiuntivo di Home Assistant" + }, + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Porta", + "ssl": "Utilizza un certificato SSL", + "username": "Nome utente", + "verify_ssl": "Verificare il certificato SSL" + }, + "description": "Configura l'istanza di AdGuard Home per consentire il monitoraggio e il controllo." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/ko.json new file mode 100644 index 00000000000..63d672a2fff --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/ko.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "existing_instance_updated": "\uae30\uc874 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "hassio_confirm": { + "description": "{addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 AdGuard Home\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Home Assistant \uc560\ub4dc\uc628\uc758 AdGuard Home" + }, + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", + "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" + }, + "description": "\ubaa8\ub2c8\ud130\ub9c1 \ubc0f \uc81c\uc5b4\uac00 \uac00\ub2a5\ud558\ub3c4\ub85d AdGuard Home \uc778\uc2a4\ud134\uc2a4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/lb.json new file mode 100644 index 00000000000..f1bd1876dc7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/lb.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "existing_instance_updated": "D\u00e9i bestehend Konfiguratioun ass ge\u00e4nnert." + }, + "error": { + "cannot_connect": "Feeler beim verbannen" + }, + "step": { + "hassio_confirm": { + "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mam AdGuard Home ze verbannen dee vum Supervisor add-on {addon} bereet gestallt g\u00ebtt?", + "title": "AdGuard Home via Supervisor add-on" + }, + "user": { + "data": { + "host": "Host", + "password": "Passwuert", + "port": "Port", + "ssl": "Benotzt een SSL Zertifikat", + "username": "Benotzernumm", + "verify_ssl": "SSL Zertifikat iwwerpr\u00e9iwen" + }, + "description": "Konfigur\u00e9iert \u00e4r AdGuard Home Instanz fir d'Iwwerwaachung an d'Kontroll z'erlaben." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/nl.json new file mode 100644 index 00000000000..9f991cbd407 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/nl.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Service is al geconfigureerd", + "existing_instance_updated": "Bestaande configuratie bijgewerkt." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "hassio_confirm": { + "description": "Wilt u Home Assistant configureren om verbinding te maken met AdGuard Home van de Home Assistant add-on: {addon}?", + "title": "AdGuard Home via Home Assistant add-on" + }, + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord", + "port": "Poort", + "ssl": "AdGuard Home maakt gebruik van een SSL certificaat", + "username": "Gebruikersnaam", + "verify_ssl": "AdGuard Home maakt gebruik van een goed certificaat" + }, + "description": "Stel uw AdGuard Home-instantie in om toezicht en controle mogelijk te maken." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/nn.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/nn.json new file mode 100644 index 00000000000..7c129cba3af --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/no.json new file mode 100644 index 00000000000..fc95d3bde66 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/no.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "existing_instance_updated": "Oppdatert eksisterende konfigurasjon." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "hassio_confirm": { + "description": "Vil du konfigurere Home Assistant til \u00e5 koble til AdGuard Home levert av tillegget: {addon} ?", + "title": "AdGuard Home via Home Assistant-tillegg" + }, + "user": { + "data": { + "host": "Vert", + "password": "Passord", + "port": "Port", + "ssl": "Bruker et SSL-sertifikat", + "username": "Brukernavn", + "verify_ssl": "Verifisere SSL-sertifikat" + }, + "description": "Sett opp din AdGuard Hjem instans for \u00e5 tillate overv\u00e5king og kontroll." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/pl.json new file mode 100644 index 00000000000..7ea17b246bc --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/pl.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "existing_instance_updated": "Zaktualizowano istniej\u0105c\u0105 konfiguracj\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "hassio_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z AdGuard Home przez dodatek {addon}?", + "title": "AdGuard Home przez dodatek Home Assistant" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "port": "Port", + "ssl": "Certyfikat SSL", + "username": "Nazwa u\u017cytkownika", + "verify_ssl": "Weryfikacja certyfikatu SSL" + }, + "description": "Skonfiguruj instancj\u0119 AdGuard Home, aby umo\u017cliwi\u0107 monitorowanie i kontrol\u0119." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/pt-BR.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/pt-BR.json new file mode 100644 index 00000000000..5d291f4cadb --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "existing_instance_updated": "Configura\u00e7\u00e3o existente atualizada." + }, + "step": { + "hassio_confirm": { + "description": "Deseja configurar o Home Assistant para se conectar ao AdGuard Home fornecido pelo complemento Supervisor: {addon} ?", + "title": "AdGuard Home via add-on Supervisor" + }, + "user": { + "data": { + "password": "Senha", + "ssl": "O AdGuard Home usa um certificado SSL", + "username": "Usu\u00e1rio", + "verify_ssl": "O AdGuard Home usa um certificado apropriado" + }, + "description": "Configure sua inst\u00e2ncia do AdGuard Home para permitir o monitoramento e o controle." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/pt.json new file mode 100644 index 00000000000..df9b6c03bc5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/pt.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "hassio_confirm": { + "title": "AdGuard Home via Supervisor add-on" + }, + "user": { + "data": { + "host": "Servidor", + "password": "Palavra-passe", + "port": "Porta", + "ssl": "Utiliza um certificado SSL", + "username": "Nome de Utilizador", + "verify_ssl": "Verificar certificado SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/ru.json new file mode 100644 index 00000000000..b1bb7d3ccf7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/ru.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "existing_instance_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\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." + }, + "step": { + "hassio_confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a AdGuard Home (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \"{addon}\")?", + "title": "AdGuard Home (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f AdGuard Home." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/sl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/sl.json new file mode 100644 index 00000000000..f878a2cc206 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/sl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "existing_instance_updated": "Posodobljena obstoje\u010da konfiguracija." + }, + "step": { + "hassio_confirm": { + "description": "\u017delite konfigurirati Home Assistant-a za povezavo z AdGuard Home, ki ga ponuja Supervisor add-on {addon} ?", + "title": "AdGuard Home preko dodatka Supervisor" + }, + "user": { + "data": { + "host": "Gostitelj", + "password": "Geslo", + "port": "Vrata", + "ssl": "AdGuard Home uporablja SSL certifikat", + "username": "Uporabni\u0161ko ime", + "verify_ssl": "AdGuard Home uporablja ustrezen certifikat" + }, + "description": "Nastavite primerek AdGuard Home, da omogo\u010dite spremljanje in nadzor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/sv.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/sv.json new file mode 100644 index 00000000000..0b58d9dcc97 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "existing_instance_updated": "Uppdaterade existerande konfiguration." + }, + "step": { + "hassio_confirm": { + "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till AdGuard Home som tillhandah\u00e5lls av Supervisor Add-on: {addon}?", + "title": "AdGuard Home via Supervisor-till\u00e4gget" + }, + "user": { + "data": { + "password": "L\u00f6senord", + "ssl": "AdGuard Home anv\u00e4nder ett SSL-certifikat", + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "AdGuard Home anv\u00e4nder ett korrekt certifikat" + }, + "description": "St\u00e4ll in din AdGuard Home-instans f\u00f6r att till\u00e5ta \u00f6vervakning och kontroll." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/tr.json new file mode 100644 index 00000000000..065af7b49cf --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/uk.json new file mode 100644 index 00000000000..34a336364a0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/uk.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "existing_instance_updated": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "hassio_confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e AdGuard Home (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor \"{addon}\")?", + "title": "AdGuard Home (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor)" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443 \u0456 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044e AdGuard Home." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/vi.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/vi.json new file mode 100644 index 00000000000..1d2ea273f93 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/vi.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "M\u1eadt kh\u1ea9u", + "username": "T\u00ean \u0111\u0103ng nh\u1eadp" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/zh-Hans.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/zh-Hans.json new file mode 100644 index 00000000000..4204beb5268 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/zh-Hans.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "existing_instance_updated": "\u66f4\u65b0\u4e86\u73b0\u6709\u914d\u7f6e\u3002" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801", + "username": "\u7528\u6237\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/zh-Hant.json new file mode 100644 index 00000000000..7db4bbcea83 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/adguard/translations/zh-Hant.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "hassio_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 AdGuard Home\u3002\u9644\u52a0\u5143\u4ef6\u70ba\uff1a{addon} \uff1f", + "title": "\u4f7f\u7528 Home Assistant \u9644\u52a0\u5143\u4ef6 AdGuard Home" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + }, + "description": "\u8a2d\u5b9a AdGuard Home \u4ee5\u9032\u884c\u76e3\u63a7\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ads/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/ads/__init__.py new file mode 100644 index 00000000000..b17a066eba7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ads/__init__.py @@ -0,0 +1,331 @@ +"""Support for Automation Device Specification (ADS).""" +import asyncio +from collections import namedtuple +import ctypes +import logging +import struct +import threading + +import async_timeout +import pyads +import voluptuous as vol + +from homeassistant.const import ( + CONF_DEVICE, + CONF_IP_ADDRESS, + CONF_PORT, + EVENT_HOMEASSISTANT_STOP, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DATA_ADS = "data_ads" + +# Supported Types +ADSTYPE_BOOL = "bool" +ADSTYPE_BYTE = "byte" +ADSTYPE_DINT = "dint" +ADSTYPE_INT = "int" +ADSTYPE_UDINT = "udint" +ADSTYPE_UINT = "uint" + +CONF_ADS_FACTOR = "factor" +CONF_ADS_TYPE = "adstype" +CONF_ADS_VALUE = "value" +CONF_ADS_VAR = "adsvar" +CONF_ADS_VAR_BRIGHTNESS = "adsvar_brightness" +CONF_ADS_VAR_POSITION = "adsvar_position" + +STATE_KEY_STATE = "state" +STATE_KEY_BRIGHTNESS = "brightness" +STATE_KEY_POSITION = "position" + +DOMAIN = "ads" + +SERVICE_WRITE_DATA_BY_NAME = "write_data_by_name" + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_DEVICE): cv.string, + vol.Required(CONF_PORT): cv.port, + vol.Optional(CONF_IP_ADDRESS): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + +SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema( + { + vol.Required(CONF_ADS_TYPE): vol.In( + [ + ADSTYPE_INT, + ADSTYPE_UINT, + ADSTYPE_BYTE, + ADSTYPE_BOOL, + ADSTYPE_DINT, + ADSTYPE_UDINT, + ] + ), + vol.Required(CONF_ADS_VALUE): vol.Coerce(int), + vol.Required(CONF_ADS_VAR): cv.string, + } +) + + +def setup(hass, config): + """Set up the ADS component.""" + + conf = config[DOMAIN] + + net_id = conf[CONF_DEVICE] + ip_address = conf.get(CONF_IP_ADDRESS) + port = conf[CONF_PORT] + + client = pyads.Connection(net_id, port, ip_address) + + AdsHub.ADS_TYPEMAP = { + ADSTYPE_BOOL: pyads.PLCTYPE_BOOL, + ADSTYPE_BYTE: pyads.PLCTYPE_BYTE, + ADSTYPE_DINT: pyads.PLCTYPE_DINT, + ADSTYPE_INT: pyads.PLCTYPE_INT, + ADSTYPE_UDINT: pyads.PLCTYPE_UDINT, + ADSTYPE_UINT: pyads.PLCTYPE_UINT, + } + + AdsHub.ADSError = pyads.ADSError + AdsHub.PLCTYPE_BOOL = pyads.PLCTYPE_BOOL + AdsHub.PLCTYPE_BYTE = pyads.PLCTYPE_BYTE + AdsHub.PLCTYPE_DINT = pyads.PLCTYPE_DINT + AdsHub.PLCTYPE_INT = pyads.PLCTYPE_INT + AdsHub.PLCTYPE_UDINT = pyads.PLCTYPE_UDINT + AdsHub.PLCTYPE_UINT = pyads.PLCTYPE_UINT + + try: + ads = AdsHub(client) + except pyads.ADSError: + _LOGGER.error( + "Could not connect to ADS host (netid=%s, ip=%s, port=%s)", + net_id, + ip_address, + port, + ) + return False + + hass.data[DATA_ADS] = ads + hass.bus.listen(EVENT_HOMEASSISTANT_STOP, ads.shutdown) + + def handle_write_data_by_name(call): + """Write a value to the connected ADS device.""" + ads_var = call.data.get(CONF_ADS_VAR) + ads_type = call.data.get(CONF_ADS_TYPE) + value = call.data.get(CONF_ADS_VALUE) + + try: + ads.write_by_name(ads_var, value, ads.ADS_TYPEMAP[ads_type]) + except pyads.ADSError as err: + _LOGGER.error(err) + + hass.services.register( + DOMAIN, + SERVICE_WRITE_DATA_BY_NAME, + handle_write_data_by_name, + schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME, + ) + + return True + + +# Tuple to hold data needed for notification +NotificationItem = namedtuple( + "NotificationItem", "hnotify huser name plc_datatype callback" +) + + +class AdsHub: + """Representation of an ADS connection.""" + + def __init__(self, ads_client): + """Initialize the ADS hub.""" + self._client = ads_client + self._client.open() + + # All ADS devices are registered here + self._devices = [] + self._notification_items = {} + self._lock = threading.Lock() + + def shutdown(self, *args, **kwargs): + """Shutdown ADS connection.""" + + _LOGGER.debug("Shutting down ADS") + for notification_item in self._notification_items.values(): + _LOGGER.debug( + "Deleting device notification %d, %d", + notification_item.hnotify, + notification_item.huser, + ) + try: + self._client.del_device_notification( + notification_item.hnotify, notification_item.huser + ) + except pyads.ADSError as err: + _LOGGER.error(err) + try: + self._client.close() + except pyads.ADSError as err: + _LOGGER.error(err) + + def register_device(self, device): + """Register a new device.""" + self._devices.append(device) + + def write_by_name(self, name, value, plc_datatype): + """Write a value to the device.""" + + with self._lock: + try: + return self._client.write_by_name(name, value, plc_datatype) + except pyads.ADSError as err: + _LOGGER.error("Error writing %s: %s", name, err) + + def read_by_name(self, name, plc_datatype): + """Read a value from the device.""" + + with self._lock: + try: + return self._client.read_by_name(name, plc_datatype) + except pyads.ADSError as err: + _LOGGER.error("Error reading %s: %s", name, err) + + def add_device_notification(self, name, plc_datatype, callback): + """Add a notification to the ADS devices.""" + + attr = pyads.NotificationAttrib(ctypes.sizeof(plc_datatype)) + + with self._lock: + try: + hnotify, huser = self._client.add_device_notification( + name, attr, self._device_notification_callback + ) + except pyads.ADSError as err: + _LOGGER.error("Error subscribing to %s: %s", name, err) + else: + hnotify = int(hnotify) + self._notification_items[hnotify] = NotificationItem( + hnotify, huser, name, plc_datatype, callback + ) + + _LOGGER.debug( + "Added device notification %d for variable %s", hnotify, name + ) + + def _device_notification_callback(self, notification, name): + """Handle device notifications.""" + contents = notification.contents + + hnotify = int(contents.hNotification) + _LOGGER.debug("Received notification %d", hnotify) + + # get dynamically sized data array + data_size = contents.cbSampleSize + data = (ctypes.c_ubyte * data_size).from_address( + ctypes.addressof(contents) + + pyads.structs.SAdsNotificationHeader.data.offset + ) + + try: + with self._lock: + notification_item = self._notification_items[hnotify] + except KeyError: + _LOGGER.error("Unknown device notification handle: %d", hnotify) + return + + # Parse data to desired datatype + if notification_item.plc_datatype == self.PLCTYPE_BOOL: + value = bool(struct.unpack("= 2: + entities.append(AdvantageAirZoneMotion(instance, ac_key, zone_key)) + # Only add MyZone if it is available + if zone["type"] != 0: + entities.append(AdvantageAirZoneMyZone(instance, ac_key, zone_key)) + async_add_entities(entities) + + +class AdvantageAirZoneFilter(AdvantageAirEntity, BinarySensorEntity): + """Advantage Air Filter.""" + + @property + def name(self): + """Return the name.""" + return f'{self._ac["name"]} Filter' + + @property + def unique_id(self): + """Return a unique id.""" + return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-filter' + + @property + def device_class(self): + """Return the device class of the vent.""" + return DEVICE_CLASS_PROBLEM + + @property + def is_on(self): + """Return if filter needs cleaning.""" + return self._ac["filterCleanStatus"] + + +class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): + """Advantage Air Zone Motion.""" + + @property + def name(self): + """Return the name.""" + return f'{self._zone["name"]} Motion' + + @property + def unique_id(self): + """Return a unique id.""" + return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-motion' + + @property + def device_class(self): + """Return the device class of the vent.""" + return DEVICE_CLASS_MOTION + + @property + def is_on(self): + """Return if motion is detect.""" + return self._zone["motion"] + + +class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity): + """Advantage Air Zone MyZone.""" + + @property + def name(self): + """Return the name.""" + return f'{self._zone["name"]} MyZone' + + @property + def unique_id(self): + """Return a unique id.""" + return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-myzone' + + @property + def is_on(self): + """Return if this zone is the myZone.""" + return self._zone["number"] == self._ac["myZone"] + + @property + def entity_registry_enabled_default(self): + """Return false to disable this entity by default.""" + return False diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/climate.py b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/climate.py new file mode 100644 index 00000000000..60caf15be25 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/climate.py @@ -0,0 +1,250 @@ +"""Climate platform for Advantage Air integration.""" + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + 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, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.helpers import entity_platform + +from .const import ( + ADVANTAGE_AIR_STATE_CLOSE, + ADVANTAGE_AIR_STATE_OFF, + ADVANTAGE_AIR_STATE_ON, + ADVANTAGE_AIR_STATE_OPEN, + DOMAIN as ADVANTAGE_AIR_DOMAIN, +) +from .entity import AdvantageAirEntity + +ADVANTAGE_AIR_HVAC_MODES = { + "heat": HVAC_MODE_HEAT, + "cool": HVAC_MODE_COOL, + "vent": HVAC_MODE_FAN_ONLY, + "dry": HVAC_MODE_DRY, +} +HASS_HVAC_MODES = {v: k for k, v in ADVANTAGE_AIR_HVAC_MODES.items()} + +ADVANTAGE_AIR_FAN_MODES = { + "auto": FAN_AUTO, + "low": FAN_LOW, + "medium": FAN_MEDIUM, + "high": FAN_HIGH, +} +HASS_FAN_MODES = {v: k for k, v in ADVANTAGE_AIR_FAN_MODES.items()} +FAN_SPEEDS = {FAN_LOW: 30, FAN_MEDIUM: 60, FAN_HIGH: 100} + +AC_HVAC_MODES = [ + HVAC_MODE_OFF, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_DRY, +] +ADVANTAGE_AIR_SERVICE_SET_MYZONE = "set_myzone" +ZONE_HVAC_MODES = [HVAC_MODE_OFF, HVAC_MODE_FAN_ONLY] + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up AdvantageAir climate platform.""" + + instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] + + entities = [] + for ac_key, ac_device in instance["coordinator"].data["aircons"].items(): + entities.append(AdvantageAirAC(instance, ac_key)) + for zone_key, zone in ac_device["zones"].items(): + # Only add zone climate control when zone is in temperature control + if zone["type"] != 0: + entities.append(AdvantageAirZone(instance, ac_key, zone_key)) + async_add_entities(entities) + + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + ADVANTAGE_AIR_SERVICE_SET_MYZONE, + {}, + "set_myzone", + ) + + +class AdvantageAirClimateEntity(AdvantageAirEntity, ClimateEntity): + """AdvantageAir Climate class.""" + + @property + def temperature_unit(self): + """Return the temperature unit.""" + return TEMP_CELSIUS + + @property + def target_temperature_step(self): + """Return the supported temperature step.""" + return PRECISION_WHOLE + + @property + def max_temp(self): + """Return the maximum supported temperature.""" + return 32 + + @property + def min_temp(self): + """Return the minimum supported temperature.""" + return 16 + + +class AdvantageAirAC(AdvantageAirClimateEntity): + """AdvantageAir AC unit.""" + + @property + def name(self): + """Return the name.""" + return self._ac["name"] + + @property + def unique_id(self): + """Return a unique id.""" + return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}' + + @property + def target_temperature(self): + """Return the current target temperature.""" + return self._ac["setTemp"] + + @property + def hvac_mode(self): + """Return the current HVAC modes.""" + if self._ac["state"] == ADVANTAGE_AIR_STATE_ON: + return ADVANTAGE_AIR_HVAC_MODES.get(self._ac["mode"]) + return HVAC_MODE_OFF + + @property + def hvac_modes(self): + """Return the supported HVAC modes.""" + return AC_HVAC_MODES + + @property + def fan_mode(self): + """Return the current fan modes.""" + return ADVANTAGE_AIR_FAN_MODES.get(self._ac["fan"]) + + @property + def fan_modes(self): + """Return the supported fan modes.""" + return [FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH] + + @property + def supported_features(self): + """Return the supported features.""" + return SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE + + async def async_set_hvac_mode(self, hvac_mode): + """Set the HVAC Mode and State.""" + if hvac_mode == HVAC_MODE_OFF: + await self.async_change( + {self.ac_key: {"info": {"state": ADVANTAGE_AIR_STATE_OFF}}} + ) + else: + await self.async_change( + { + self.ac_key: { + "info": { + "state": ADVANTAGE_AIR_STATE_ON, + "mode": HASS_HVAC_MODES.get(hvac_mode), + } + } + } + ) + + async def async_set_fan_mode(self, fan_mode): + """Set the Fan Mode.""" + await self.async_change( + {self.ac_key: {"info": {"fan": HASS_FAN_MODES.get(fan_mode)}}} + ) + + async def async_set_temperature(self, **kwargs): + """Set the Temperature.""" + temp = kwargs.get(ATTR_TEMPERATURE) + await self.async_change({self.ac_key: {"info": {"setTemp": temp}}}) + + +class AdvantageAirZone(AdvantageAirClimateEntity): + """AdvantageAir Zone control.""" + + @property + def name(self): + """Return the name.""" + return self._zone["name"] + + @property + def unique_id(self): + """Return a unique id.""" + return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}' + + @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"] + + @property + def hvac_mode(self): + """Return the current HVAC modes.""" + if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN: + return HVAC_MODE_FAN_ONLY + return HVAC_MODE_OFF + + @property + def hvac_modes(self): + """Return supported HVAC modes.""" + return ZONE_HVAC_MODES + + @property + def supported_features(self): + """Return the supported features.""" + return SUPPORT_TARGET_TEMPERATURE + + async def async_set_hvac_mode(self, hvac_mode): + """Set the HVAC Mode and State.""" + if hvac_mode == HVAC_MODE_OFF: + await self.async_change( + { + self.ac_key: { + "zones": {self.zone_key: {"state": ADVANTAGE_AIR_STATE_CLOSE}} + } + } + ) + else: + await self.async_change( + { + self.ac_key: { + "zones": {self.zone_key: {"state": ADVANTAGE_AIR_STATE_OPEN}} + } + } + ) + + async def async_set_temperature(self, **kwargs): + """Set the Temperature.""" + temp = kwargs.get(ATTR_TEMPERATURE) + await self.async_change( + {self.ac_key: {"zones": {self.zone_key: {"setTemp": temp}}}} + ) + + async def set_myzone(self, **kwargs): + """Set this zone as the 'MyZone'.""" + await self.async_change( + {self.ac_key: {"info": {"myZone": self._zone["number"]}}} + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/config_flow.py new file mode 100644 index 00000000000..b13ab1e9b21 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/config_flow.py @@ -0,0 +1,57 @@ +"""Config Flow for Advantage Air integration.""" +from advantage_air import ApiError, advantage_air +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import ADVANTAGE_AIR_RETRY, DOMAIN + +ADVANTAGE_AIR_DEFAULT_PORT = 2025 + +ADVANTAGE_AIR_SCHEMA = vol.Schema( + { + vol.Required(CONF_IP_ADDRESS): str, + vol.Optional(CONF_PORT, default=ADVANTAGE_AIR_DEFAULT_PORT): int, + } +) + + +class AdvantageAirConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Config Advantage Air API connection.""" + + VERSION = 1 + + DOMAIN = DOMAIN + + async def async_step_user(self, user_input=None): + """Get configuration from the user.""" + errors = {} + if user_input: + ip_address = user_input[CONF_IP_ADDRESS] + port = user_input[CONF_PORT] + + try: + data = await advantage_air( + ip_address, + port=port, + session=async_get_clientsession(self.hass), + retry=ADVANTAGE_AIR_RETRY, + ).async_get(1) + except ApiError: + errors["base"] = "cannot_connect" + else: + await self.async_set_unique_id(data["system"]["rid"]) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=data["system"]["name"], + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=ADVANTAGE_AIR_SCHEMA, + errors=errors, + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/const.py new file mode 100644 index 00000000000..5c044481ca0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/const.py @@ -0,0 +1,7 @@ +"""Constants used by Advantage Air integration.""" +DOMAIN = "advantage_air" +ADVANTAGE_AIR_RETRY = 10 +ADVANTAGE_AIR_STATE_OPEN = "open" +ADVANTAGE_AIR_STATE_CLOSE = "close" +ADVANTAGE_AIR_STATE_ON = "on" +ADVANTAGE_AIR_STATE_OFF = "off" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/cover.py b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/cover.py new file mode 100644 index 00000000000..69d66849cd6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/cover.py @@ -0,0 +1,116 @@ +"""Cover platform for Advantage Air integration.""" + +from homeassistant.components.cover import ( + ATTR_POSITION, + DEVICE_CLASS_DAMPER, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + CoverEntity, +) + +from .const import ( + ADVANTAGE_AIR_STATE_CLOSE, + ADVANTAGE_AIR_STATE_OPEN, + DOMAIN as ADVANTAGE_AIR_DOMAIN, +) +from .entity import AdvantageAirEntity + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up AdvantageAir cover platform.""" + + instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] + + entities = [] + for ac_key, ac_device in instance["coordinator"].data["aircons"].items(): + for zone_key, zone in ac_device["zones"].items(): + # Only add zone vent controls when zone in vent control mode. + if zone["type"] == 0: + entities.append(AdvantageAirZoneVent(instance, ac_key, zone_key)) + async_add_entities(entities) + + +class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity): + """Advantage Air Cover Class.""" + + @property + def name(self): + """Return the name.""" + return f'{self._zone["name"]}' + + @property + def unique_id(self): + """Return a unique id.""" + return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}' + + @property + def device_class(self): + """Return the device class of the vent.""" + return DEVICE_CLASS_DAMPER + + @property + def supported_features(self): + """Return the supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION + + @property + def is_closed(self): + """Return if vent is fully closed.""" + return self._zone["state"] == ADVANTAGE_AIR_STATE_CLOSE + + @property + def current_cover_position(self): + """Return vents current position as a percentage.""" + if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN: + return self._zone["value"] + return 0 + + async def async_open_cover(self, **kwargs): + """Fully open zone vent.""" + await self.async_change( + { + self.ac_key: { + "zones": { + self.zone_key: {"state": ADVANTAGE_AIR_STATE_OPEN, "value": 100} + } + } + } + ) + + async def async_close_cover(self, **kwargs): + """Fully close zone vent.""" + await self.async_change( + { + self.ac_key: { + "zones": {self.zone_key: {"state": ADVANTAGE_AIR_STATE_CLOSE}} + } + } + ) + + async def async_set_cover_position(self, **kwargs): + """Change vent position.""" + position = round(kwargs[ATTR_POSITION] / 5) * 5 + if position == 0: + await self.async_change( + { + self.ac_key: { + "zones": {self.zone_key: {"state": ADVANTAGE_AIR_STATE_CLOSE}} + } + } + ) + else: + await self.async_change( + { + self.ac_key: { + "zones": { + self.zone_key: { + "state": ADVANTAGE_AIR_STATE_OPEN, + "value": position, + } + } + } + } + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/entity.py b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/entity.py new file mode 100644 index 00000000000..ea20368c10f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/entity.py @@ -0,0 +1,35 @@ +"""Advantage Air parent entity class.""" + +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN + + +class AdvantageAirEntity(CoordinatorEntity): + """Parent class for Advantage Air Entities.""" + + def __init__(self, instance, ac_key, zone_key=None): + """Initialize common aspects of an Advantage Air sensor.""" + super().__init__(instance["coordinator"]) + self.async_change = instance["async_change"] + self.ac_key = ac_key + self.zone_key = zone_key + + @property + def _ac(self): + return self.coordinator.data["aircons"][self.ac_key]["info"] + + @property + def _zone(self): + return self.coordinator.data["aircons"][self.ac_key]["zones"][self.zone_key] + + @property + def device_info(self): + """Return parent device information.""" + return { + "identifiers": {(DOMAIN, self.coordinator.data["system"]["rid"])}, + "name": self.coordinator.data["system"]["name"], + "manufacturer": "Advantage Air", + "model": self.coordinator.data["system"]["sysType"], + "sw_version": self.coordinator.data["system"]["myAppRev"], + } diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/manifest.json new file mode 100644 index 00000000000..750d5457e17 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "advantage_air", + "name": "Advantage Air", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/advantage_air", + "codeowners": ["@Bre77"], + "requirements": ["advantage_air==0.2.1"], + "quality_scale": "platinum", + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/sensor.py new file mode 100644 index 00000000000..8f027b1bdaf --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/sensor.py @@ -0,0 +1,153 @@ +"""Sensor platform for Advantage Air integration.""" +import voluptuous as vol + +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import PERCENTAGE +from homeassistant.helpers import config_validation as cv, entity_platform + +from .const import ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN +from .entity import AdvantageAirEntity + +ADVANTAGE_AIR_SET_COUNTDOWN_VALUE = "minutes" +ADVANTAGE_AIR_SET_COUNTDOWN_UNIT = "min" +ADVANTAGE_AIR_SERVICE_SET_TIME_TO = "set_time_to" + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up AdvantageAir sensor platform.""" + + instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] + + entities = [] + for ac_key, ac_device in instance["coordinator"].data["aircons"].items(): + entities.append(AdvantageAirTimeTo(instance, ac_key, "On")) + entities.append(AdvantageAirTimeTo(instance, ac_key, "Off")) + for zone_key, zone in ac_device["zones"].items(): + # Only show damper sensors when zone is in temperature control + if zone["type"] != 0: + entities.append(AdvantageAirZoneVent(instance, ac_key, zone_key)) + # Only show wireless signal strength sensors when using wireless sensors + if zone["rssi"] > 0: + entities.append(AdvantageAirZoneSignal(instance, ac_key, zone_key)) + async_add_entities(entities) + + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + ADVANTAGE_AIR_SERVICE_SET_TIME_TO, + {vol.Required("minutes"): cv.positive_int}, + "set_time_to", + ) + + +class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): + """Representation of Advantage Air timer control.""" + + def __init__(self, instance, ac_key, action): + """Initialize the Advantage Air timer control.""" + super().__init__(instance, ac_key) + self.action = action + self._time_key = f"countDownTo{self.action}" + + @property + def name(self): + """Return the name.""" + return f'{self._ac["name"]} Time To {self.action}' + + @property + def unique_id(self): + """Return a unique id.""" + return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-timeto{self.action}' + + @property + def state(self): + """Return the current value.""" + return self._ac[self._time_key] + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return ADVANTAGE_AIR_SET_COUNTDOWN_UNIT + + @property + def icon(self): + """Return a representative icon of the timer.""" + if self._ac[self._time_key] > 0: + return "mdi:timer-outline" + return "mdi:timer-off-outline" + + async def set_time_to(self, **kwargs): + """Set the timer value.""" + value = min(720, max(0, int(kwargs[ADVANTAGE_AIR_SET_COUNTDOWN_VALUE]))) + await self.async_change({self.ac_key: {"info": {self._time_key: value}}}) + + +class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): + """Representation of Advantage Air Zone Vent Sensor.""" + + @property + def name(self): + """Return the name.""" + return f'{self._zone["name"]} Vent' + + @property + def unique_id(self): + """Return a unique id.""" + return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-vent' + + @property + def state(self): + """Return the current value of the air vent.""" + if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN: + return self._zone["value"] + return 0 + + @property + def unit_of_measurement(self): + """Return the percent sign.""" + return PERCENTAGE + + @property + def icon(self): + """Return a representative icon.""" + if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN: + return "mdi:fan" + return "mdi:fan-off" + + +class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): + """Representation of Advantage Air Zone wireless signal sensor.""" + + @property + def name(self): + """Return the name.""" + return f'{self._zone["name"]} Signal' + + @property + def unique_id(self): + """Return a unique id.""" + return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-signal' + + @property + def state(self): + """Return the current value of the wireless signal.""" + return self._zone["rssi"] + + @property + def unit_of_measurement(self): + """Return the percent sign.""" + return PERCENTAGE + + @property + def icon(self): + """Return a representative icon.""" + if self._zone["rssi"] >= 80: + return "mdi:wifi-strength-4" + if self._zone["rssi"] >= 60: + return "mdi:wifi-strength-3" + if self._zone["rssi"] >= 40: + return "mdi:wifi-strength-2" + if self._zone["rssi"] >= 20: + return "mdi:wifi-strength-1" + return "mdi:wifi-strength-outline" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/services.yaml b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/services.yaml new file mode 100644 index 00000000000..24088421c99 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/services.yaml @@ -0,0 +1,26 @@ +set_time_to: + name: Set Time To + description: Control timers to turn the system on or off after a set number of minutes + target: + entity: + integration: advantage_air + domain: sensor + fields: + minutes: + name: Minutes + description: Minutes until action + required: true + example: "60" + selector: + number: + min: 0 + max: 1440 + unit_of_measurement: minutes + +set_myzone: + name: Set MyZone + description: Change which zone is set as the reference for temperature control + target: + entity: + integration: advantage_air + domain: climate diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/strings.json new file mode 100644 index 00000000000..76ecb174f6d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "step": { + "user": { + "data": { + "ip_address": "[%key:common::config_flow::data::ip%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "description": "Connect to the API of your Advantage Air wall mounted tablet.", + "title": "Connect" + } + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/switch.py b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/switch.py new file mode 100644 index 00000000000..6c687c1427e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/switch.py @@ -0,0 +1,58 @@ +"""Switch platform for Advantage Air integration.""" + +from homeassistant.helpers.entity import ToggleEntity + +from .const import ( + ADVANTAGE_AIR_STATE_OFF, + ADVANTAGE_AIR_STATE_ON, + DOMAIN as ADVANTAGE_AIR_DOMAIN, +) +from .entity import AdvantageAirEntity + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up AdvantageAir toggle platform.""" + + instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] + + entities = [] + for ac_key, ac_device in instance["coordinator"].data["aircons"].items(): + if ac_device["info"]["freshAirStatus"] != "none": + entities.append(AdvantageAirFreshAir(instance, ac_key)) + async_add_entities(entities) + + +class AdvantageAirFreshAir(AdvantageAirEntity, ToggleEntity): + """Representation of Advantage Air fresh air control.""" + + @property + def name(self): + """Return the name.""" + return f'{self._ac["name"]} Fresh Air' + + @property + def unique_id(self): + """Return a unique id.""" + return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-freshair' + + @property + def is_on(self): + """Return the fresh air status.""" + return self._ac["freshAirStatus"] == ADVANTAGE_AIR_STATE_ON + + @property + def icon(self): + """Return a representative icon of the fresh air switch.""" + return "mdi:air-filter" + + async def async_turn_on(self, **kwargs): + """Turn fresh air on.""" + await self.async_change( + {self.ac_key: {"info": {"freshAirStatus": ADVANTAGE_AIR_STATE_ON}}} + ) + + async def async_turn_off(self, **kwargs): + """Turn fresh air off.""" + await self.async_change( + {self.ac_key: {"info": {"freshAirStatus": ADVANTAGE_AIR_STATE_OFF}}} + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/ca.json new file mode 100644 index 00000000000..7702b3b0588 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "ip_address": "Adre\u00e7a IP", + "port": "Port" + }, + "description": "Connecta't a l'API de la tauleta d'Advantage Air.", + "title": "Connecta" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/cs.json new file mode 100644 index 00000000000..e7823abb4f5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresa", + "port": "Port" + }, + "description": "P\u0159ipojte se k API va\u0161eho n\u00e1st\u011bnn\u00e9ho tabletu Advantage Air.", + "title": "P\u0159ipojit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/de.json new file mode 100644 index 00000000000..c761ac5c6be --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "ip_address": "IP Adresse", + "port": "Port" + }, + "description": "Anschluss an die API Ihres Advantage Air Wandtabletts.", + "title": "Verbinden" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/en.json new file mode 100644 index 00000000000..de715c3ae1b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "ip_address": "IP Address", + "port": "Port" + }, + "description": "Connect to the API of your Advantage Air wall mounted tablet.", + "title": "Connect" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/es-419.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/es-419.json new file mode 100644 index 00000000000..f2f9a463527 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/es-419.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Conectar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/es.json new file mode 100644 index 00000000000..8cd125526df --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "ip_address": "Direcci\u00f3n IP", + "port": "Puerto" + }, + "description": "Con\u00e9ctate a la API de tu tableta de pared Advantage Air.", + "title": "Conectar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/et.json new file mode 100644 index 00000000000..900265465e8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendus nurjus" + }, + "step": { + "user": { + "data": { + "ip_address": "IP aadress", + "port": "" + }, + "description": "\u00dchendu oma Advantage Air seinale paigaldatud tahvelarvuti API-ga.", + "title": "\u00dchenda" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/fr.json new file mode 100644 index 00000000000..e5f6c52ee2c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "user": { + "data": { + "ip_address": "Adresse IP", + "port": "Port" + }, + "description": "Connectez-vous \u00e0 l'API de votre tablette murale Advantage Air.", + "title": "Connecter" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/hu.json new file mode 100644 index 00000000000..e82e88da8d2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "ip_address": "IP c\u00edm", + "port": "Port" + }, + "description": "Csatlakozzon az Advantage Air fali t\u00e1blag\u00e9p API-j\u00e1hoz.", + "title": "Csatlakoz\u00e1s" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/id.json new file mode 100644 index 00000000000..7993fa3be1d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "ip_address": "Alamat IP", + "port": "Port" + }, + "description": "Hubungkan ke API tablet dinding Advantage Air.", + "title": "Hubungkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/it.json new file mode 100644 index 00000000000..c9ea54d85dc --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "ip_address": "Indirizzo IP", + "port": "Porta" + }, + "description": "Connettiti all'API del tablet montato a parete di Advantage Air.", + "title": "Connetti" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/ka.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/ka.json new file mode 100644 index 00000000000..4216ece47d2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/ka.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \u10db\u10d8\u10e1\u10d0\u10db\u10d0\u10e0\u10d7\u10d8", + "port": "\u10de\u10dd\u10e0\u10e2\u10d8" + }, + "description": "\u10d3\u10d0\u10e3\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d3\u10d8\u10d7 \u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Advantage Air API-\u10e1 \u10d9\u10d4\u10d3\u10d4\u10da\u10d6\u10d4 \u10d3\u10d0\u10db\u10dd\u10dc\u10e2\u10d0\u10df\u10d4\u10d1\u10e3\u10da\u10d8 \u10e2\u10d0\u10d1\u10da\u10d4\u10e2\u10d8\u10d7", + "title": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d4\u10d1\u10d0" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/ko.json new file mode 100644 index 00000000000..9a28cc499bc --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \uc8fc\uc18c", + "port": "\ud3ec\ud2b8" + }, + "description": "\ubcbd\uc5d0 \ubd80\ucc29\ub41c Advantage Air \ud0dc\ube14\ub9bf\uc758 API\uc5d0 \uc5f0\uacb0\ud558\uae30", + "title": "\uc5f0\uacb0\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/lb.json new file mode 100644 index 00000000000..48e58149930 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/lb.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen" + }, + "step": { + "user": { + "data": { + "ip_address": "IP Adresse", + "port": "Port" + }, + "description": "Mat der API vun dengem Advantage Air Tablet verbannen", + "title": "Verbannen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/nl.json new file mode 100644 index 00000000000..3206c7a3165 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "ip_address": "IP-adres", + "port": "Poort" + }, + "description": "Maak verbinding met de API van uw Advantage Air-tablet voor wandmontage.", + "title": "Verbind" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/no.json new file mode 100644 index 00000000000..9ed185a7a7a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresse", + "port": "Port" + }, + "description": "Koble til API p\u00e5 din Advantage Air veggmonterte nettbrett.", + "title": "Koble til" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/pl.json new file mode 100644 index 00000000000..ef2d1909b9f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "ip_address": "Adres IP", + "port": "Port" + }, + "description": "Po\u0142\u0105cz si\u0119 z API tabletu firmy Advantage Air.", + "title": "Po\u0142\u0105czenie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/pt.json new file mode 100644 index 00000000000..37e27fd8394 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP", + "port": "Porta" + }, + "title": "Ligar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/ru.json new file mode 100644 index 00000000000..b978329896d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/ru.json @@ -0,0 +1,20 @@ +{ + "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." + }, + "step": { + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a API \u0412\u0430\u0448\u0435\u0433\u043e \u043d\u0430\u0441\u0442\u0435\u043d\u043d\u043e\u0433\u043e \u043f\u043b\u0430\u043d\u0448\u0435\u0442\u0430 Advantage Air.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/sl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/sl.json new file mode 100644 index 00000000000..3e080b3db31 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/sl.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Pove\u017eite se" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/tr.json new file mode 100644 index 00000000000..db639c59376 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "ip_address": "\u0130p Adresi", + "port": "Port" + }, + "title": "Ba\u011flan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/uk.json new file mode 100644 index 00000000000..14ac18395e2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e API \u0412\u0430\u0448\u043e\u0433\u043e \u043d\u0430\u0441\u0442\u0456\u043d\u043d\u043e\u0433\u043e \u043f\u043b\u0430\u043d\u0448\u0435\u0442\u0430 Advantage Air.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/zh-Hans.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/zh-Hans.json new file mode 100644 index 00000000000..db79116d5ea --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/zh-Hans.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + }, + "step": { + "user": { + "data": { + "ip_address": "IP\u5730\u5740" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/zh-Hant.json new file mode 100644 index 00000000000..a6d7280b069 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/advantage_air/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \u4f4d\u5740", + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u9023\u7dda\u81f3 Advantage Air \u58c1\u639b\u5e73\u677f API\u3002", + "title": "\u9023\u63a5" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/__init__.py new file mode 100644 index 00000000000..879f59fa2fc --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/__init__.py @@ -0,0 +1,64 @@ +"""The AEMET OpenData component.""" +import logging + +from aemet_opendata.interface import AEMET + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant + +from .const import ( + CONF_STATION_UPDATES, + DOMAIN, + ENTRY_NAME, + ENTRY_WEATHER_COORDINATOR, + PLATFORMS, +) +from .weather_update_coordinator import WeatherUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Set up AEMET OpenData as config entry.""" + name = config_entry.data[CONF_NAME] + api_key = config_entry.data[CONF_API_KEY] + latitude = config_entry.data[CONF_LATITUDE] + longitude = config_entry.data[CONF_LONGITUDE] + station_updates = config_entry.options.get(CONF_STATION_UPDATES, True) + + aemet = AEMET(api_key) + weather_coordinator = WeatherUpdateCoordinator( + hass, aemet, latitude, longitude, station_updates + ) + + await weather_coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][config_entry.entry_id] = { + ENTRY_NAME: name, + ENTRY_WEATHER_COORDINATOR: weather_coordinator, + } + + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + + config_entry.async_on_unload(config_entry.add_update_listener(async_update_options)) + + return True + + +async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Update options.""" + await hass.config_entries.async_reload(config_entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + + if unload_ok: + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/config_flow.py new file mode 100644 index 00000000000..6c97ca98cb8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/config_flow.py @@ -0,0 +1,85 @@ +"""Config flow for AEMET OpenData.""" +from aemet_opendata import AEMET +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv + +from .const import CONF_STATION_UPDATES, DEFAULT_NAME, DOMAIN + + +class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for AEMET OpenData.""" + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + latitude = user_input[CONF_LATITUDE] + longitude = user_input[CONF_LONGITUDE] + + await self.async_set_unique_id(f"{latitude}-{longitude}") + self._abort_if_unique_id_configured() + + api_online = await _is_aemet_api_online(self.hass, user_input[CONF_API_KEY]) + if not api_online: + errors["base"] = "invalid_api_key" + + if not errors: + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) + + schema = vol.Schema( + { + vol.Required(CONF_API_KEY): str, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, + vol.Optional( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Optional( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + } + ) + + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for AEMET.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + data_schema = vol.Schema( + { + vol.Required( + CONF_STATION_UPDATES, + default=self.config_entry.options.get(CONF_STATION_UPDATES), + ): bool, + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) + + +async def _is_aemet_api_online(hass, api_key): + aemet = AEMET(api_key) + return await hass.async_add_executor_job( + aemet.get_conventional_observation_stations, False + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/const.py new file mode 100644 index 00000000000..0927f64dd2a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/const.py @@ -0,0 +1,326 @@ +"""Constant values for the AEMET OpenData component.""" + +from homeassistant.components.weather import ( + ATTR_CONDITION_CLEAR_NIGHT, + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_FOG, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SUNNY, + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, +) +from homeassistant.const import ( + DEGREE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, + PERCENTAGE, + PRECIPITATION_MILLIMETERS_PER_HOUR, + PRESSURE_HPA, + SPEED_KILOMETERS_PER_HOUR, + TEMP_CELSIUS, +) + +ATTRIBUTION = "Powered by AEMET OpenData" +CONF_STATION_UPDATES = "station_updates" +PLATFORMS = ["sensor", "weather"] +DEFAULT_NAME = "AEMET" +DOMAIN = "aemet" +ENTRY_NAME = "name" +ENTRY_WEATHER_COORDINATOR = "weather_coordinator" +SENSOR_NAME = "sensor_name" +SENSOR_UNIT = "sensor_unit" +SENSOR_DEVICE_CLASS = "sensor_device_class" + +ATTR_API_CONDITION = "condition" +ATTR_API_FORECAST_DAILY = "forecast-daily" +ATTR_API_FORECAST_HOURLY = "forecast-hourly" +ATTR_API_HUMIDITY = "humidity" +ATTR_API_PRESSURE = "pressure" +ATTR_API_RAIN = "rain" +ATTR_API_RAIN_PROB = "rain-probability" +ATTR_API_SNOW = "snow" +ATTR_API_SNOW_PROB = "snow-probability" +ATTR_API_STATION_ID = "station-id" +ATTR_API_STATION_NAME = "station-name" +ATTR_API_STATION_TIMESTAMP = "station-timestamp" +ATTR_API_STORM_PROB = "storm-probability" +ATTR_API_TEMPERATURE = "temperature" +ATTR_API_TEMPERATURE_FEELING = "temperature-feeling" +ATTR_API_TOWN_ID = "town-id" +ATTR_API_TOWN_NAME = "town-name" +ATTR_API_TOWN_TIMESTAMP = "town-timestamp" +ATTR_API_WIND_BEARING = "wind-bearing" +ATTR_API_WIND_MAX_SPEED = "wind-max-speed" +ATTR_API_WIND_SPEED = "wind-speed" + +CONDITIONS_MAP = { + ATTR_CONDITION_CLEAR_NIGHT: { + "11n", # Despejado (de noche) + }, + ATTR_CONDITION_CLOUDY: { + "14", # Nuboso + "14n", # Nuboso (de noche) + "15", # Muy nuboso + "15n", # Muy nuboso (de noche) + "16", # Cubierto + "16n", # Cubierto (de noche) + "17", # Nubes altas + "17n", # Nubes altas (de noche) + }, + ATTR_CONDITION_FOG: { + "81", # Niebla + "81n", # Niebla (de noche) + "82", # Bruma - Neblina + "82n", # Bruma - Neblina (de noche) + }, + ATTR_CONDITION_LIGHTNING: { + "51", # Intervalos nubosos con tormenta + "51n", # Intervalos nubosos con tormenta (de noche) + "52", # Nuboso con tormenta + "52n", # Nuboso con tormenta (de noche) + "53", # Muy nuboso con tormenta + "53n", # Muy nuboso con tormenta (de noche) + "54", # Cubierto con tormenta + "54n", # Cubierto con tormenta (de noche) + }, + ATTR_CONDITION_LIGHTNING_RAINY: { + "61", # Intervalos nubosos con tormenta y lluvia escasa + "61n", # Intervalos nubosos con tormenta y lluvia escasa (de noche) + "62", # Nuboso con tormenta y lluvia escasa + "62n", # Nuboso con tormenta y lluvia escasa (de noche) + "63", # Muy nuboso con tormenta y lluvia escasa + "63n", # Muy nuboso con tormenta y lluvia escasa (de noche) + "64", # Cubierto con tormenta y lluvia escasa + "64n", # Cubierto con tormenta y lluvia escasa (de noche) + }, + ATTR_CONDITION_PARTLYCLOUDY: { + "12", # Poco nuboso + "12n", # Poco nuboso (de noche) + "13", # Intervalos nubosos + "13n", # Intervalos nubosos (de noche) + }, + ATTR_CONDITION_POURING: { + "27", # Chubascos + "27n", # Chubascos (de noche) + }, + ATTR_CONDITION_RAINY: { + "23", # Intervalos nubosos con lluvia + "23n", # Intervalos nubosos con lluvia (de noche) + "24", # Nuboso con lluvia + "24n", # Nuboso con lluvia (de noche) + "25", # Muy nuboso con lluvia + "25n", # Muy nuboso con lluvia (de noche) + "26", # Cubierto con lluvia + "26n", # Cubierto con lluvia (de noche) + "43", # Intervalos nubosos con lluvia escasa + "43n", # Intervalos nubosos con lluvia escasa (de noche) + "44", # Nuboso con lluvia escasa + "44n", # Nuboso con lluvia escasa (de noche) + "45", # Muy nuboso con lluvia escasa + "45n", # Muy nuboso con lluvia escasa (de noche) + "46", # Cubierto con lluvia escasa + "46n", # Cubierto con lluvia escasa (de noche) + }, + ATTR_CONDITION_SNOWY: { + "33", # Intervalos nubosos con nieve + "33n", # Intervalos nubosos con nieve (de noche) + "34", # Nuboso con nieve + "34n", # Nuboso con nieve (de noche) + "35", # Muy nuboso con nieve + "35n", # Muy nuboso con nieve (de noche) + "36", # Cubierto con nieve + "36n", # Cubierto con nieve (de noche) + "71", # Intervalos nubosos con nieve escasa + "71n", # Intervalos nubosos con nieve escasa (de noche) + "72", # Nuboso con nieve escasa + "72n", # Nuboso con nieve escasa (de noche) + "73", # Muy nuboso con nieve escasa + "73n", # Muy nuboso con nieve escasa (de noche) + "74", # Cubierto con nieve escasa + "74n", # Cubierto con nieve escasa (de noche) + }, + ATTR_CONDITION_SUNNY: { + "11", # Despejado + }, +} + +FORECAST_MONITORED_CONDITIONS = [ + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, +] +MONITORED_CONDITIONS = [ + ATTR_API_CONDITION, + ATTR_API_HUMIDITY, + ATTR_API_PRESSURE, + ATTR_API_RAIN, + ATTR_API_RAIN_PROB, + ATTR_API_SNOW, + ATTR_API_SNOW_PROB, + ATTR_API_STATION_ID, + ATTR_API_STATION_NAME, + ATTR_API_STATION_TIMESTAMP, + ATTR_API_STORM_PROB, + ATTR_API_TEMPERATURE, + ATTR_API_TEMPERATURE_FEELING, + ATTR_API_TOWN_ID, + ATTR_API_TOWN_NAME, + ATTR_API_TOWN_TIMESTAMP, + ATTR_API_WIND_BEARING, + ATTR_API_WIND_MAX_SPEED, + ATTR_API_WIND_SPEED, +] + +FORECAST_MODE_DAILY = "daily" +FORECAST_MODE_HOURLY = "hourly" +FORECAST_MODES = [ + FORECAST_MODE_DAILY, + FORECAST_MODE_HOURLY, +] +FORECAST_MODE_ATTR_API = { + FORECAST_MODE_DAILY: ATTR_API_FORECAST_DAILY, + FORECAST_MODE_HOURLY: ATTR_API_FORECAST_HOURLY, +} + +FORECAST_SENSOR_TYPES = { + ATTR_FORECAST_CONDITION: { + SENSOR_NAME: "Condition", + }, + ATTR_FORECAST_PRECIPITATION: { + SENSOR_NAME: "Precipitation", + SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR, + }, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: { + SENSOR_NAME: "Precipitation probability", + SENSOR_UNIT: PERCENTAGE, + }, + ATTR_FORECAST_TEMP: { + SENSOR_NAME: "Temperature", + SENSOR_UNIT: TEMP_CELSIUS, + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + }, + ATTR_FORECAST_TEMP_LOW: { + SENSOR_NAME: "Temperature Low", + SENSOR_UNIT: TEMP_CELSIUS, + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + }, + ATTR_FORECAST_TIME: { + SENSOR_NAME: "Time", + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + }, + ATTR_FORECAST_WIND_BEARING: { + SENSOR_NAME: "Wind bearing", + SENSOR_UNIT: DEGREE, + }, + ATTR_FORECAST_WIND_SPEED: { + SENSOR_NAME: "Wind speed", + SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR, + }, +} +WEATHER_SENSOR_TYPES = { + ATTR_API_CONDITION: { + SENSOR_NAME: "Condition", + }, + ATTR_API_HUMIDITY: { + SENSOR_NAME: "Humidity", + SENSOR_UNIT: PERCENTAGE, + SENSOR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + }, + ATTR_API_PRESSURE: { + SENSOR_NAME: "Pressure", + SENSOR_UNIT: PRESSURE_HPA, + SENSOR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + }, + ATTR_API_RAIN: { + SENSOR_NAME: "Rain", + SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR, + }, + ATTR_API_RAIN_PROB: { + SENSOR_NAME: "Rain probability", + SENSOR_UNIT: PERCENTAGE, + }, + ATTR_API_SNOW: { + SENSOR_NAME: "Snow", + SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR, + }, + ATTR_API_SNOW_PROB: { + SENSOR_NAME: "Snow probability", + SENSOR_UNIT: PERCENTAGE, + }, + ATTR_API_STATION_ID: { + SENSOR_NAME: "Station ID", + }, + ATTR_API_STATION_NAME: { + SENSOR_NAME: "Station name", + }, + ATTR_API_STATION_TIMESTAMP: { + SENSOR_NAME: "Station timestamp", + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + }, + ATTR_API_STORM_PROB: { + SENSOR_NAME: "Storm probability", + SENSOR_UNIT: PERCENTAGE, + }, + ATTR_API_TEMPERATURE: { + SENSOR_NAME: "Temperature", + SENSOR_UNIT: TEMP_CELSIUS, + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + }, + ATTR_API_TEMPERATURE_FEELING: { + SENSOR_NAME: "Temperature feeling", + SENSOR_UNIT: TEMP_CELSIUS, + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + }, + ATTR_API_TOWN_ID: { + SENSOR_NAME: "Town ID", + }, + ATTR_API_TOWN_NAME: { + SENSOR_NAME: "Town name", + }, + ATTR_API_TOWN_TIMESTAMP: { + SENSOR_NAME: "Town timestamp", + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + }, + ATTR_API_WIND_BEARING: { + SENSOR_NAME: "Wind bearing", + SENSOR_UNIT: DEGREE, + }, + ATTR_API_WIND_MAX_SPEED: { + SENSOR_NAME: "Wind max speed", + SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR, + }, + ATTR_API_WIND_SPEED: { + SENSOR_NAME: "Wind speed", + SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR, + }, +} + +WIND_BEARING_MAP = { + "C": None, + "N": 0.0, + "NE": 45.0, + "E": 90.0, + "SE": 135.0, + "S": 180.0, + "SO": 225.0, + "O": 270.0, + "NO": 315.0, +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/manifest.json new file mode 100644 index 00000000000..8f33e9dbf03 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "aemet", + "name": "AEMET OpenData", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/aemet", + "requirements": ["AEMET-OpenData==0.2.1"], + "codeowners": ["@noltari"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/sensor.py new file mode 100644 index 00000000000..de7b06347c3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/sensor.py @@ -0,0 +1,168 @@ +"""Support for the AEMET OpenData service.""" +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ( + ATTRIBUTION, + DOMAIN, + ENTRY_NAME, + ENTRY_WEATHER_COORDINATOR, + FORECAST_MODE_ATTR_API, + FORECAST_MODE_DAILY, + FORECAST_MODES, + FORECAST_MONITORED_CONDITIONS, + FORECAST_SENSOR_TYPES, + MONITORED_CONDITIONS, + SENSOR_DEVICE_CLASS, + SENSOR_NAME, + SENSOR_UNIT, + WEATHER_SENSOR_TYPES, +) +from .weather_update_coordinator import WeatherUpdateCoordinator + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up AEMET OpenData sensor entities based on a config entry.""" + domain_data = hass.data[DOMAIN][config_entry.entry_id] + name = domain_data[ENTRY_NAME] + weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR] + + weather_sensor_types = WEATHER_SENSOR_TYPES + forecast_sensor_types = FORECAST_SENSOR_TYPES + + entities = [] + for sensor_type in MONITORED_CONDITIONS: + unique_id = f"{config_entry.unique_id}-{sensor_type}" + entities.append( + AemetSensor( + name, + unique_id, + sensor_type, + weather_sensor_types[sensor_type], + weather_coordinator, + ) + ) + + for mode in FORECAST_MODES: + name = f"{domain_data[ENTRY_NAME]} {mode}" + + for sensor_type in FORECAST_MONITORED_CONDITIONS: + unique_id = f"{config_entry.unique_id}-forecast-{mode}-{sensor_type}" + entities.append( + AemetForecastSensor( + f"{name} Forecast", + unique_id, + sensor_type, + forecast_sensor_types[sensor_type], + weather_coordinator, + mode, + ) + ) + + async_add_entities(entities) + + +class AbstractAemetSensor(CoordinatorEntity, SensorEntity): + """Abstract class for an AEMET OpenData sensor.""" + + def __init__( + self, + name, + unique_id, + sensor_type, + sensor_configuration, + coordinator: WeatherUpdateCoordinator, + ): + """Initialize the sensor.""" + super().__init__(coordinator) + self._name = name + self._unique_id = unique_id + self._sensor_type = sensor_type + self._sensor_name = sensor_configuration[SENSOR_NAME] + self._unit_of_measurement = sensor_configuration.get(SENSOR_UNIT) + self._device_class = sensor_configuration.get(SENSOR_DEVICE_CLASS) + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self._name} {self._sensor_name}" + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return self._unique_id + + @property + def device_class(self): + """Return the device_class.""" + return self._device_class + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + +class AemetSensor(AbstractAemetSensor): + """Implementation of an AEMET OpenData sensor.""" + + def __init__( + self, + name, + unique_id, + sensor_type, + sensor_configuration, + weather_coordinator: WeatherUpdateCoordinator, + ): + """Initialize the sensor.""" + super().__init__( + name, unique_id, sensor_type, sensor_configuration, weather_coordinator + ) + self._weather_coordinator = weather_coordinator + + @property + def state(self): + """Return the state of the device.""" + return self._weather_coordinator.data.get(self._sensor_type) + + +class AemetForecastSensor(AbstractAemetSensor): + """Implementation of an AEMET OpenData forecast sensor.""" + + def __init__( + self, + name, + unique_id, + sensor_type, + sensor_configuration, + weather_coordinator: WeatherUpdateCoordinator, + forecast_mode, + ): + """Initialize the sensor.""" + super().__init__( + name, unique_id, sensor_type, sensor_configuration, weather_coordinator + ) + self._weather_coordinator = weather_coordinator + self._forecast_mode = forecast_mode + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self._forecast_mode == FORECAST_MODE_DAILY + + @property + def state(self): + """Return the state of the device.""" + forecast = None + forecasts = self._weather_coordinator.data.get( + FORECAST_MODE_ATTR_API[self._forecast_mode] + ) + if forecasts: + forecast = forecasts[0].get(self._sensor_type) + return forecast diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/strings.json new file mode 100644 index 00000000000..360f7c680ea --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/strings.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + }, + "error": { + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" + }, + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]", + "name": "Name of the integration" + }, + "description": "Set up AEMET OpenData integration. To generate API key go to https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "AEMET OpenData" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Gather data from AEMET weather stations" + } + } + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/ca.json new file mode 100644 index 00000000000..75784ddfc87 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/ca.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada" + }, + "error": { + "invalid_api_key": "Clau API inv\u00e0lida" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nom de la integraci\u00f3" + }, + "description": "Configura la integraci\u00f3 d'AEMET OpenData. Per generar la clau API, v\u00e9s a https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "AEMET OpenData" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Obt\u00e9 les dades de les estacions meteorol\u00f2giques d'AEMET" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/cs.json new file mode 100644 index 00000000000..d892d4c6dc3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno" + }, + "error": { + "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API" + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", + "name": "N\u00e1zev integrace" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/de.json new file mode 100644 index 00000000000..d5312805722 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Standort ist bereits konfiguriert" + }, + "error": { + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "name": "Name der Integration" + }, + "description": "Richte die AEMET OpenData Integration ein. Um den API-Schl\u00fcssel zu generieren, besuche https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "[void]" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/en.json new file mode 100644 index 00000000000..3888ccdafc0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/en.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Location is already configured" + }, + "error": { + "invalid_api_key": "Invalid API key" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name of the integration" + }, + "description": "Set up AEMET OpenData integration. To generate API key go to https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "AEMET OpenData" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Gather data from AEMET weather stations" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/es-419.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/es-419.json new file mode 100644 index 00000000000..4b3db0a8833 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/es-419.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nombre de la integraci\u00f3n" + }, + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/es.json new file mode 100644 index 00000000000..ffe4d524754 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada" + }, + "error": { + "invalid_api_key": "Clave API no v\u00e1lida" + }, + "step": { + "user": { + "data": { + "api_key": "Clave API", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre de la integraci\u00f3n" + }, + "description": "Configurar la integraci\u00f3n de AEMET OpenData. Para generar la clave API, ve a https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/et.json new file mode 100644 index 00000000000..0d542fcc744 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/et.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Asukoht on juba m\u00e4\u00e4ratud" + }, + "error": { + "invalid_api_key": "Vale API v\u00f5ti" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad", + "name": "Sidumise nimi" + }, + "description": "Seadista AEMET OpenData sidumine. API v\u00f5tme loomiseks mine aadressile https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "AEMET OpenData" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Koguandmeid AEMETi ilmajaamadest" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/fr.json new file mode 100644 index 00000000000..bb1e792aa5e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "invalid_api_key": "Cl\u00e9 API invalide" + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nom de l'int\u00e9gration" + }, + "description": "Configurez l'int\u00e9gration AEMET OpenData. Pour g\u00e9n\u00e9rer la cl\u00e9 API, acc\u00e9dez \u00e0 https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/hu.json new file mode 100644 index 00000000000..d810691046e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "name": "Az integr\u00e1ci\u00f3 neve" + }, + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/id.json new file mode 100644 index 00000000000..fa678cbbbe0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Lokasi sudah dikonfigurasi" + }, + "error": { + "invalid_api_key": "Kunci API tidak valid" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "latitude": "Lintang", + "longitude": "Bujur", + "name": "Nama integrasi" + }, + "description": "Siapkan integrasi AEMET OpenData. Untuk menghasilkan kunci API, buka https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/it.json new file mode 100644 index 00000000000..a55e003ca4e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/it.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "La posizione \u00e8 gi\u00e0 configurata" + }, + "error": { + "invalid_api_key": "Chiave API non valida" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "latitude": "Latitudine", + "longitude": "Logitudine", + "name": "Nome dell'integrazione" + }, + "description": "Imposta l'integrazione di AEMET OpenData. Per generare la chiave API, vai su https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "AEMET OpenData" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Raccogli i dati dalle stazioni meteorologiche AEMET" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/ko.json new file mode 100644 index 00000000000..95c11b018fe --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "api_key": "API \ud0a4", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc774\ub984" + }, + "description": "AEMET OpenData \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://opendata.aemet.es/centrodedescargas/altaUsuario \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694", + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/lb.json new file mode 100644 index 00000000000..8e83c0e86d3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/lb.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API Schl\u00ebssel", + "latitude": "L\u00e4ngegrad", + "longitude": "Breedegrad", + "name": "Numm vun der Integratioun" + }, + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/nl.json new file mode 100644 index 00000000000..40fab5d9e0f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/nl.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Locatie is al geconfigureerd." + }, + "error": { + "invalid_api_key": "Ongeldige API-sleutel" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "name": "Naam van de integratie" + }, + "description": "Stel AEMET OpenData-integratie in. Ga naar https://opendata.aemet.es/centrodedescargas/altaUsuario om een API-sleutel te genereren", + "title": "AEMET OpenData" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Verzamel gegevens van AEMET-weerstations" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/no.json new file mode 100644 index 00000000000..48cbc9916ca --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Plasseringen er allerede konfigurert" + }, + "error": { + "invalid_api_key": "Ugyldig API-n\u00f8kkel" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "name": "Navnet p\u00e5 integrasjonen" + }, + "description": "Sett opp AEMET OpenData-integrasjon. For \u00e5 generere API-n\u00f8kkel, g\u00e5 til https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/pl.json new file mode 100644 index 00000000000..2c5c24fae2a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Lokalizacja jest ju\u017c skonfigurowana" + }, + "error": { + "invalid_api_key": "Nieprawid\u0142owy klucz API" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa integracji" + }, + "description": "Skonfiguruj integracj\u0119 AEMET OpenData. Aby wygenerowa\u0107 klucz API, przejd\u017a do https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/ru.json new file mode 100644 index 00000000000..1dc0e21b0df --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/ru.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 AEMET OpenData. \u0427\u0442\u043e\u0431\u044b \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 https://opendata.aemet.es/centrodedescargas/altaUsuario.", + "title": "AEMET OpenData" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "\u0421\u0431\u043e\u0440 \u0434\u0430\u043d\u043d\u044b\u0445 \u0441 \u043c\u0435\u0442\u0435\u043e\u0441\u0442\u0430\u043d\u0446\u0438\u0439 AEMET" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/zh-Hant.json new file mode 100644 index 00000000000..e2b1eef10b9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/translations/zh-Hant.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "invalid_api_key": "API \u5bc6\u9470\u7121\u6548" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u6574\u5408\u540d\u7a31" + }, + "description": "\u6b32\u8a2d\u5b9a AEMET OpenData \u6574\u5408\u3002\u8acb\u81f3 https://opendata.aemet.es/centrodedescargas/altaUsuario \u7522\u751f API \u5bc6\u9470", + "title": "AEMET OpenData" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "\u81ea AEMET \u6c23\u8c61\u7ad9\u7372\u5f97\u8cc7\u6599" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/weather.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/weather.py new file mode 100644 index 00000000000..e54a297cc09 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/weather.py @@ -0,0 +1,113 @@ +"""Support for the AEMET OpenData service.""" +from homeassistant.components.weather import WeatherEntity +from homeassistant.const import TEMP_CELSIUS +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ( + ATTR_API_CONDITION, + ATTR_API_HUMIDITY, + ATTR_API_PRESSURE, + ATTR_API_TEMPERATURE, + ATTR_API_WIND_BEARING, + ATTR_API_WIND_SPEED, + ATTRIBUTION, + DOMAIN, + ENTRY_NAME, + ENTRY_WEATHER_COORDINATOR, + FORECAST_MODE_ATTR_API, + FORECAST_MODE_DAILY, + FORECAST_MODES, +) +from .weather_update_coordinator import WeatherUpdateCoordinator + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up AEMET OpenData weather entity based on a config entry.""" + domain_data = hass.data[DOMAIN][config_entry.entry_id] + weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR] + + entities = [] + for mode in FORECAST_MODES: + name = f"{domain_data[ENTRY_NAME]} {mode}" + unique_id = f"{config_entry.unique_id} {mode}" + entities.append(AemetWeather(name, unique_id, weather_coordinator, mode)) + + if entities: + async_add_entities(entities, False) + + +class AemetWeather(CoordinatorEntity, WeatherEntity): + """Implementation of an AEMET OpenData sensor.""" + + def __init__( + self, + name, + unique_id, + coordinator: WeatherUpdateCoordinator, + forecast_mode, + ): + """Initialize the sensor.""" + super().__init__(coordinator) + self._name = name + self._unique_id = unique_id + self._forecast_mode = forecast_mode + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def condition(self): + """Return the current condition.""" + return self.coordinator.data[ATTR_API_CONDITION] + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self._forecast_mode == FORECAST_MODE_DAILY + + @property + def forecast(self): + """Return the forecast array.""" + return self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]] + + @property + def humidity(self): + """Return the humidity.""" + return self.coordinator.data[ATTR_API_HUMIDITY] + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def pressure(self): + """Return the pressure.""" + return self.coordinator.data[ATTR_API_PRESSURE] + + @property + def temperature(self): + """Return the temperature.""" + return self.coordinator.data[ATTR_API_TEMPERATURE] + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return self._unique_id + + @property + def wind_bearing(self): + """Return the temperature.""" + return self.coordinator.data[ATTR_API_WIND_BEARING] + + @property + def wind_speed(self): + """Return the temperature.""" + return self.coordinator.data[ATTR_API_WIND_SPEED] diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/weather_update_coordinator.py new file mode 100644 index 00000000000..8259baf9984 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aemet/weather_update_coordinator.py @@ -0,0 +1,643 @@ +"""Weather data coordinator for the AEMET OpenData service.""" +from dataclasses import dataclass, field +from datetime import timedelta +import logging + +from aemet_opendata.const import ( + AEMET_ATTR_DATE, + AEMET_ATTR_DAY, + AEMET_ATTR_DIRECTION, + AEMET_ATTR_ELABORATED, + AEMET_ATTR_FORECAST, + AEMET_ATTR_HUMIDITY, + AEMET_ATTR_ID, + AEMET_ATTR_IDEMA, + AEMET_ATTR_MAX, + AEMET_ATTR_MIN, + AEMET_ATTR_NAME, + AEMET_ATTR_PRECIPITATION, + AEMET_ATTR_PRECIPITATION_PROBABILITY, + AEMET_ATTR_SKY_STATE, + AEMET_ATTR_SNOW, + AEMET_ATTR_SNOW_PROBABILITY, + AEMET_ATTR_SPEED, + AEMET_ATTR_STATION_DATE, + AEMET_ATTR_STATION_HUMIDITY, + AEMET_ATTR_STATION_LOCATION, + AEMET_ATTR_STATION_PRESSURE_SEA, + AEMET_ATTR_STATION_TEMPERATURE, + AEMET_ATTR_STORM_PROBABILITY, + AEMET_ATTR_TEMPERATURE, + AEMET_ATTR_TEMPERATURE_FEELING, + AEMET_ATTR_WIND, + AEMET_ATTR_WIND_GUST, + ATTR_DATA, +) +from aemet_opendata.helpers import ( + get_forecast_day_value, + get_forecast_hour_value, + get_forecast_interval_value, +) +import async_timeout + +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, +) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt as dt_util + +from .const import ( + ATTR_API_CONDITION, + ATTR_API_FORECAST_DAILY, + ATTR_API_FORECAST_HOURLY, + ATTR_API_HUMIDITY, + ATTR_API_PRESSURE, + ATTR_API_RAIN, + ATTR_API_RAIN_PROB, + ATTR_API_SNOW, + ATTR_API_SNOW_PROB, + ATTR_API_STATION_ID, + ATTR_API_STATION_NAME, + ATTR_API_STATION_TIMESTAMP, + ATTR_API_STORM_PROB, + ATTR_API_TEMPERATURE, + ATTR_API_TEMPERATURE_FEELING, + ATTR_API_TOWN_ID, + ATTR_API_TOWN_NAME, + ATTR_API_TOWN_TIMESTAMP, + ATTR_API_WIND_BEARING, + ATTR_API_WIND_MAX_SPEED, + ATTR_API_WIND_SPEED, + CONDITIONS_MAP, + DOMAIN, + WIND_BEARING_MAP, +) + +_LOGGER = logging.getLogger(__name__) + +STATION_MAX_DELTA = timedelta(hours=2) +WEATHER_UPDATE_INTERVAL = timedelta(minutes=10) + + +def format_condition(condition: str) -> str: + """Return condition from dict CONDITIONS_MAP.""" + for key, value in CONDITIONS_MAP.items(): + if condition in value: + return key + _LOGGER.error('Condition "%s" not found in CONDITIONS_MAP', condition) + return condition + + +def format_float(value) -> float: + """Try converting string to float.""" + try: + return float(value) + except (TypeError, ValueError): + return None + + +def format_int(value) -> int: + """Try converting string to int.""" + try: + return int(value) + except (TypeError, ValueError): + return None + + +class TownNotFound(UpdateFailed): + """Raised when town is not found.""" + + +class WeatherUpdateCoordinator(DataUpdateCoordinator): + """Weather data update coordinator.""" + + def __init__(self, hass, aemet, latitude, longitude, station_updates): + """Initialize coordinator.""" + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=WEATHER_UPDATE_INTERVAL + ) + + self._aemet = aemet + self._station = None + self._town = None + self._latitude = latitude + self._longitude = longitude + self._station_updates = station_updates + self._data = { + "daily": None, + "hourly": None, + "station": None, + } + + async def _async_update_data(self): + data = {} + with async_timeout.timeout(120): + weather_response = await self._get_aemet_weather() + data = self._convert_weather_response(weather_response) + return data + + async def _get_aemet_weather(self): + """Poll weather data from AEMET OpenData.""" + weather = await self.hass.async_add_executor_job(self._get_weather_and_forecast) + return weather + + def _get_weather_station(self): + if not self._station: + self._station = ( + self._aemet.get_conventional_observation_station_by_coordinates( + self._latitude, self._longitude + ) + ) + if self._station: + _LOGGER.debug( + "station found for coordinates [%s, %s]: %s", + self._latitude, + self._longitude, + self._station, + ) + if not self._station: + _LOGGER.debug( + "station not found for coordinates [%s, %s]", + self._latitude, + self._longitude, + ) + return self._station + + def _get_weather_town(self): + if not self._town: + self._town = self._aemet.get_town_by_coordinates( + self._latitude, self._longitude + ) + if self._town: + _LOGGER.debug( + "Town found for coordinates [%s, %s]: %s", + self._latitude, + self._longitude, + self._town, + ) + if not self._town: + _LOGGER.error( + "Town not found for coordinates [%s, %s]", + self._latitude, + self._longitude, + ) + raise TownNotFound + return self._town + + def _get_weather_and_forecast(self): + """Get weather and forecast data from AEMET OpenData.""" + + self._get_weather_town() + + daily = self._aemet.get_specific_forecast_town_daily(self._town[AEMET_ATTR_ID]) + if not daily: + _LOGGER.error( + 'Error fetching daily data for town "%s"', self._town[AEMET_ATTR_ID] + ) + + hourly = self._aemet.get_specific_forecast_town_hourly( + self._town[AEMET_ATTR_ID] + ) + if not hourly: + _LOGGER.error( + 'Error fetching hourly data for town "%s"', self._town[AEMET_ATTR_ID] + ) + + station = None + if self._station_updates and self._get_weather_station(): + station = self._aemet.get_conventional_observation_station_data( + self._station[AEMET_ATTR_IDEMA] + ) + if not station: + _LOGGER.error( + 'Error fetching data for station "%s"', + self._station[AEMET_ATTR_IDEMA], + ) + + if daily: + self._data["daily"] = daily + if hourly: + self._data["hourly"] = hourly + if station: + self._data["station"] = station + + return AemetWeather( + self._data["daily"], + self._data["hourly"], + self._data["station"], + ) + + def _convert_weather_response(self, weather_response): + """Format the weather response correctly.""" + if not weather_response or not weather_response.hourly: + return None + + elaborated = dt_util.parse_datetime( + weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_ELABORATED] + "Z" + ) + now = dt_util.now() + now_utc = dt_util.utcnow() + hour = now.hour + + # Get current day + day = None + for cur_day in weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_FORECAST][ + AEMET_ATTR_DAY + ]: + cur_day_date = dt_util.parse_datetime(cur_day[AEMET_ATTR_DATE]) + if now.date() == cur_day_date.date(): + day = cur_day + break + + # Get latest station data + station_data = None + station_dt = None + if weather_response.station: + for _station_data in weather_response.station[ATTR_DATA]: + if AEMET_ATTR_STATION_DATE in _station_data: + _station_dt = dt_util.parse_datetime( + _station_data[AEMET_ATTR_STATION_DATE] + "Z" + ) + if not station_dt or _station_dt > station_dt: + station_data = _station_data + station_dt = _station_dt + + condition = None + humidity = None + pressure = None + rain = None + rain_prob = None + snow = None + snow_prob = None + station_id = None + station_name = None + station_timestamp = None + storm_prob = None + temperature = None + temperature_feeling = None + town_id = None + town_name = None + town_timestamp = dt_util.as_utc(elaborated).isoformat() + wind_bearing = None + wind_max_speed = None + wind_speed = None + + # Get weather values + if day: + condition = self._get_condition(day, hour) + humidity = self._get_humidity(day, hour) + rain = self._get_rain(day, hour) + rain_prob = self._get_rain_prob(day, hour) + snow = self._get_snow(day, hour) + snow_prob = self._get_snow_prob(day, hour) + station_id = self._get_station_id() + station_name = self._get_station_name() + storm_prob = self._get_storm_prob(day, hour) + temperature = self._get_temperature(day, hour) + temperature_feeling = self._get_temperature_feeling(day, hour) + town_id = self._get_town_id() + town_name = self._get_town_name() + wind_bearing = self._get_wind_bearing(day, hour) + wind_max_speed = self._get_wind_max_speed(day, hour) + wind_speed = self._get_wind_speed(day, hour) + + # Overwrite weather values with closest station data (if present) + if station_data: + station_timestamp = dt_util.as_utc(station_dt).isoformat() + if (now_utc - station_dt) <= STATION_MAX_DELTA: + if AEMET_ATTR_STATION_HUMIDITY in station_data: + humidity = format_float(station_data[AEMET_ATTR_STATION_HUMIDITY]) + if AEMET_ATTR_STATION_PRESSURE_SEA in station_data: + pressure = format_float( + station_data[AEMET_ATTR_STATION_PRESSURE_SEA] + ) + if AEMET_ATTR_STATION_TEMPERATURE in station_data: + temperature = format_float( + station_data[AEMET_ATTR_STATION_TEMPERATURE] + ) + else: + _LOGGER.warning("Station data is outdated") + + # Get forecast from weather data + forecast_daily = self._get_daily_forecast_from_weather_response( + weather_response, now + ) + forecast_hourly = self._get_hourly_forecast_from_weather_response( + weather_response, now + ) + + return { + ATTR_API_CONDITION: condition, + ATTR_API_FORECAST_DAILY: forecast_daily, + ATTR_API_FORECAST_HOURLY: forecast_hourly, + ATTR_API_HUMIDITY: humidity, + ATTR_API_TEMPERATURE: temperature, + ATTR_API_TEMPERATURE_FEELING: temperature_feeling, + ATTR_API_PRESSURE: pressure, + ATTR_API_RAIN: rain, + ATTR_API_RAIN_PROB: rain_prob, + ATTR_API_SNOW: snow, + ATTR_API_SNOW_PROB: snow_prob, + ATTR_API_STATION_ID: station_id, + ATTR_API_STATION_NAME: station_name, + ATTR_API_STATION_TIMESTAMP: station_timestamp, + ATTR_API_STORM_PROB: storm_prob, + ATTR_API_TOWN_ID: town_id, + ATTR_API_TOWN_NAME: town_name, + ATTR_API_TOWN_TIMESTAMP: town_timestamp, + ATTR_API_WIND_BEARING: wind_bearing, + ATTR_API_WIND_MAX_SPEED: wind_max_speed, + ATTR_API_WIND_SPEED: wind_speed, + } + + def _get_daily_forecast_from_weather_response(self, weather_response, now): + if weather_response.daily: + parse = False + forecast = [] + for day in weather_response.daily[ATTR_DATA][0][AEMET_ATTR_FORECAST][ + AEMET_ATTR_DAY + ]: + day_date = dt_util.parse_datetime(day[AEMET_ATTR_DATE]) + if now.date() == day_date.date(): + parse = True + if parse: + cur_forecast = self._convert_forecast_day(day_date, day) + if cur_forecast: + forecast.append(cur_forecast) + return forecast + return None + + def _get_hourly_forecast_from_weather_response(self, weather_response, now): + if weather_response.hourly: + parse = False + hour = now.hour + forecast = [] + for day in weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_FORECAST][ + AEMET_ATTR_DAY + ]: + day_date = dt_util.parse_datetime(day[AEMET_ATTR_DATE]) + hour_start = 0 + if now.date() == day_date.date(): + parse = True + hour_start = now.hour + if parse: + for hour in range(hour_start, 24): + cur_forecast = self._convert_forecast_hour(day_date, day, hour) + if cur_forecast: + forecast.append(cur_forecast) + return forecast + return None + + def _convert_forecast_day(self, date, day): + condition = self._get_condition_day(day) + if not condition: + return None + + return { + ATTR_FORECAST_CONDITION: condition, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day( + day + ), + ATTR_FORECAST_TEMP: self._get_temperature_day(day), + ATTR_FORECAST_TEMP_LOW: self._get_temperature_low_day(day), + ATTR_FORECAST_TIME: dt_util.as_utc(date).isoformat(), + ATTR_FORECAST_WIND_SPEED: self._get_wind_speed_day(day), + ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day), + } + + def _convert_forecast_hour(self, date, day, hour): + condition = self._get_condition(day, hour) + if not condition: + return None + + forecast_dt = date.replace(hour=hour, minute=0, second=0) + + return { + ATTR_FORECAST_CONDITION: condition, + ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour), + ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob( + day, hour + ), + ATTR_FORECAST_TEMP: self._get_temperature(day, hour), + ATTR_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(), + ATTR_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour), + ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour), + } + + def _calc_precipitation(self, day, hour): + """Calculate the precipitation.""" + rain_value = self._get_rain(day, hour) + if not rain_value: + rain_value = 0 + + snow_value = self._get_snow(day, hour) + if not snow_value: + snow_value = 0 + + if round(rain_value + snow_value, 1) == 0: + return None + return round(rain_value + snow_value, 1) + + def _calc_precipitation_prob(self, day, hour): + """Calculate the precipitation probability (hour).""" + rain_value = self._get_rain_prob(day, hour) + if not rain_value: + rain_value = 0 + + snow_value = self._get_snow_prob(day, hour) + if not snow_value: + snow_value = 0 + + if rain_value == 0 and snow_value == 0: + return None + return max(rain_value, snow_value) + + @staticmethod + def _get_condition(day_data, hour): + """Get weather condition (hour) from weather data.""" + val = get_forecast_hour_value(day_data[AEMET_ATTR_SKY_STATE], hour) + if val: + return format_condition(val) + return None + + @staticmethod + def _get_condition_day(day_data): + """Get weather condition (day) from weather data.""" + val = get_forecast_day_value(day_data[AEMET_ATTR_SKY_STATE]) + if val: + return format_condition(val) + return None + + @staticmethod + def _get_humidity(day_data, hour): + """Get humidity from weather data.""" + val = get_forecast_hour_value(day_data[AEMET_ATTR_HUMIDITY], hour) + if val: + return format_int(val) + return None + + @staticmethod + def _get_precipitation_prob_day(day_data): + """Get humidity from weather data.""" + val = get_forecast_day_value(day_data[AEMET_ATTR_PRECIPITATION_PROBABILITY]) + if val: + return format_int(val) + return None + + @staticmethod + def _get_rain(day_data, hour): + """Get rain from weather data.""" + val = get_forecast_hour_value(day_data[AEMET_ATTR_PRECIPITATION], hour) + if val: + return format_float(val) + return None + + @staticmethod + def _get_rain_prob(day_data, hour): + """Get rain probability from weather data.""" + val = get_forecast_interval_value( + day_data[AEMET_ATTR_PRECIPITATION_PROBABILITY], hour + ) + if val: + return format_int(val) + return None + + @staticmethod + def _get_snow(day_data, hour): + """Get snow from weather data.""" + val = get_forecast_hour_value(day_data[AEMET_ATTR_SNOW], hour) + if val: + return format_float(val) + return None + + @staticmethod + def _get_snow_prob(day_data, hour): + """Get snow probability from weather data.""" + val = get_forecast_interval_value(day_data[AEMET_ATTR_SNOW_PROBABILITY], hour) + if val: + return format_int(val) + return None + + def _get_station_id(self): + """Get station ID from weather data.""" + if self._station: + return self._station[AEMET_ATTR_IDEMA] + return None + + def _get_station_name(self): + """Get station name from weather data.""" + if self._station: + return self._station[AEMET_ATTR_STATION_LOCATION] + return None + + @staticmethod + def _get_storm_prob(day_data, hour): + """Get storm probability from weather data.""" + val = get_forecast_interval_value(day_data[AEMET_ATTR_STORM_PROBABILITY], hour) + if val: + return format_int(val) + return None + + @staticmethod + def _get_temperature(day_data, hour): + """Get temperature (hour) from weather data.""" + val = get_forecast_hour_value(day_data[AEMET_ATTR_TEMPERATURE], hour) + return format_int(val) + + @staticmethod + def _get_temperature_day(day_data): + """Get temperature (day) from weather data.""" + val = get_forecast_day_value( + day_data[AEMET_ATTR_TEMPERATURE], key=AEMET_ATTR_MAX + ) + return format_int(val) + + @staticmethod + def _get_temperature_low_day(day_data): + """Get temperature (day) from weather data.""" + val = get_forecast_day_value( + day_data[AEMET_ATTR_TEMPERATURE], key=AEMET_ATTR_MIN + ) + return format_int(val) + + @staticmethod + def _get_temperature_feeling(day_data, hour): + """Get temperature from weather data.""" + val = get_forecast_hour_value(day_data[AEMET_ATTR_TEMPERATURE_FEELING], hour) + return format_int(val) + + def _get_town_id(self): + """Get town ID from weather data.""" + if self._town: + return self._town[AEMET_ATTR_ID] + return None + + def _get_town_name(self): + """Get town name from weather data.""" + if self._town: + return self._town[AEMET_ATTR_NAME] + return None + + @staticmethod + def _get_wind_bearing(day_data, hour): + """Get wind bearing (hour) from weather data.""" + val = get_forecast_hour_value( + day_data[AEMET_ATTR_WIND_GUST], hour, key=AEMET_ATTR_DIRECTION + )[0] + if val in WIND_BEARING_MAP: + return WIND_BEARING_MAP[val] + _LOGGER.error("%s not found in Wind Bearing map", val) + return None + + @staticmethod + def _get_wind_bearing_day(day_data): + """Get wind bearing (day) from weather data.""" + val = get_forecast_day_value( + day_data[AEMET_ATTR_WIND], key=AEMET_ATTR_DIRECTION + ) + if val in WIND_BEARING_MAP: + return WIND_BEARING_MAP[val] + _LOGGER.error("%s not found in Wind Bearing map", val) + return None + + @staticmethod + def _get_wind_max_speed(day_data, hour): + """Get wind max speed from weather data.""" + val = get_forecast_hour_value(day_data[AEMET_ATTR_WIND_GUST], hour) + if val: + return format_int(val) + return None + + @staticmethod + def _get_wind_speed(day_data, hour): + """Get wind speed (hour) from weather data.""" + val = get_forecast_hour_value( + day_data[AEMET_ATTR_WIND_GUST], hour, key=AEMET_ATTR_SPEED + )[0] + if val: + return format_int(val) + return None + + @staticmethod + def _get_wind_speed_day(day_data): + """Get wind speed (day) from weather data.""" + val = get_forecast_day_value(day_data[AEMET_ATTR_WIND], key=AEMET_ATTR_SPEED) + if val: + return format_int(val) + return None + + +@dataclass +class AemetWeather: + """Class to harmonize weather data model.""" + + daily: dict = field(default_factory=dict) + hourly: dict = field(default_factory=dict) + station: dict = field(default_factory=dict) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aftership/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aftership/__init__.py new file mode 100644 index 00000000000..b063c919f18 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aftership/__init__.py @@ -0,0 +1 @@ +"""The aftership component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aftership/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aftership/const.py new file mode 100644 index 00000000000..d0176cde15d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aftership/const.py @@ -0,0 +1,42 @@ +"""Constants for the Aftership integration.""" +from __future__ import annotations + +from datetime import timedelta +from typing import Final + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv + +DOMAIN: Final = "aftership" + +ATTRIBUTION: Final = "Information provided by AfterShip" +ATTR_TRACKINGS: Final = "trackings" + +BASE: Final = "https://track.aftership.com/" + +CONF_SLUG: Final = "slug" +CONF_TITLE: Final = "title" +CONF_TRACKING_NUMBER: Final = "tracking_number" + +DEFAULT_NAME: Final = "aftership" +UPDATE_TOPIC: Final = f"{DOMAIN}_update" + +ICON: Final = "mdi:package-variant-closed" + +MIN_TIME_BETWEEN_UPDATES: Final = timedelta(minutes=15) + +SERVICE_ADD_TRACKING: Final = "add_tracking" +SERVICE_REMOVE_TRACKING: Final = "remove_tracking" + +ADD_TRACKING_SERVICE_SCHEMA: Final = vol.Schema( + { + vol.Required(CONF_TRACKING_NUMBER): cv.string, + vol.Optional(CONF_TITLE): cv.string, + vol.Optional(CONF_SLUG): cv.string, + } +) + +REMOVE_TRACKING_SERVICE_SCHEMA: Final = vol.Schema( + {vol.Required(CONF_SLUG): cv.string, vol.Required(CONF_TRACKING_NUMBER): cv.string} +) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aftership/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aftership/manifest.json new file mode 100644 index 00000000000..5308d08be50 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aftership/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "aftership", + "name": "AfterShip", + "documentation": "https://www.home-assistant.io/integrations/aftership", + "requirements": ["pyaftership==0.1.2"], + "codeowners": [], + "iot_class": "cloud_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aftership/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aftership/sensor.py new file mode 100644 index 00000000000..4d3fb17b949 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aftership/sensor.py @@ -0,0 +1,211 @@ +"""Support for non-delivered packages recorded in AfterShip.""" +from __future__ import annotations + +import logging +from typing import Any, Final + +from pyaftership.tracker import Tracking +import voluptuous as vol + +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, + SensorEntity, +) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME, HTTP_OK +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.service import ServiceCall +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util import Throttle + +from .const import ( + ADD_TRACKING_SERVICE_SCHEMA, + ATTR_TRACKINGS, + ATTRIBUTION, + BASE, + CONF_SLUG, + CONF_TITLE, + CONF_TRACKING_NUMBER, + DEFAULT_NAME, + DOMAIN, + ICON, + MIN_TIME_BETWEEN_UPDATES, + REMOVE_TRACKING_SERVICE_SCHEMA, + SERVICE_ADD_TRACKING, + SERVICE_REMOVE_TRACKING, + UPDATE_TOPIC, +) + +_LOGGER: Final = logging.getLogger(__name__) + +PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the AfterShip sensor platform.""" + apikey = config[CONF_API_KEY] + name = config[CONF_NAME] + + session = async_get_clientsession(hass) + aftership = Tracking(hass.loop, session, apikey) + + await aftership.get_trackings() + + if not aftership.meta or aftership.meta["code"] != HTTP_OK: + _LOGGER.error( + "No tracking data found. Check API key is correct: %s", aftership.meta + ) + return + + instance = AfterShipSensor(aftership, name) + + async_add_entities([instance], True) + + async def handle_add_tracking(call: ServiceCall) -> None: + """Call when a user adds a new Aftership tracking from Home Assistant.""" + title = call.data.get(CONF_TITLE) + slug = call.data.get(CONF_SLUG) + tracking_number = call.data[CONF_TRACKING_NUMBER] + + await aftership.add_package_tracking(tracking_number, title, slug) + async_dispatcher_send(hass, UPDATE_TOPIC) + + hass.services.async_register( + DOMAIN, + SERVICE_ADD_TRACKING, + handle_add_tracking, + schema=ADD_TRACKING_SERVICE_SCHEMA, + ) + + async def handle_remove_tracking(call: ServiceCall) -> None: + """Call when a user removes an Aftership tracking from Home Assistant.""" + slug = call.data[CONF_SLUG] + tracking_number = call.data[CONF_TRACKING_NUMBER] + + await aftership.remove_package_tracking(slug, tracking_number) + async_dispatcher_send(hass, UPDATE_TOPIC) + + hass.services.async_register( + DOMAIN, + SERVICE_REMOVE_TRACKING, + handle_remove_tracking, + schema=REMOVE_TRACKING_SERVICE_SCHEMA, + ) + + +class AfterShipSensor(SensorEntity): + """Representation of a AfterShip sensor.""" + + def __init__(self, aftership: Tracking, name: str) -> None: + """Initialize the sensor.""" + self._attributes: dict[str, Any] = {} + self._name: str = name + self._state: int | None = None + self.aftership = aftership + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return self._name + + @property + def state(self) -> int | None: + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self) -> str: + """Return the unit of measurement of this entity, if any.""" + return "packages" + + @property + def extra_state_attributes(self) -> dict[str, str]: + """Return attributes for the sensor.""" + return self._attributes + + @property + def icon(self) -> str: + """Icon to use in the frontend.""" + return ICON + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + UPDATE_TOPIC, self._force_update + ) + ) + + async def _force_update(self) -> None: + """Force update of data.""" + await self.async_update(no_throttle=True) + self.async_write_ha_state() + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self, **kwargs: Any) -> None: + """Get the latest data from the AfterShip API.""" + await self.aftership.get_trackings() + + if not self.aftership.meta: + _LOGGER.error("Unknown errors when querying") + return + if self.aftership.meta["code"] != HTTP_OK: + _LOGGER.error( + "Errors when querying AfterShip. %s", str(self.aftership.meta) + ) + return + + status_to_ignore = {"delivered"} + status_counts: dict[str, int] = {} + trackings = [] + not_delivered_count = 0 + + for track in self.aftership.trackings["trackings"]: + status = track["tag"].lower() + name = ( + track["tracking_number"] if track["title"] is None else track["title"] + ) + last_checkpoint = ( + f"Shipment {track['tag'].lower()}" + if not track["checkpoints"] + else track["checkpoints"][-1] + ) + status_counts[status] = status_counts.get(status, 0) + 1 + trackings.append( + { + "name": name, + "tracking_number": track["tracking_number"], + "slug": track["slug"], + "link": f"{BASE}{track['slug']}/{track['tracking_number']}", + "last_update": track["updated_at"], + "expected_delivery": track["expected_delivery"], + "status": track["tag"], + "last_checkpoint": last_checkpoint, + } + ) + + if status not in status_to_ignore: + not_delivered_count += 1 + else: + _LOGGER.debug("Ignoring %s as it has status: %s", name, status) + + self._attributes = { + ATTR_ATTRIBUTION: ATTRIBUTION, + **status_counts, + ATTR_TRACKINGS: trackings, + } + + self._state = not_delivered_count diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aftership/services.yaml b/homeassistant-2021.6.0.dev0/homeassistant/components/aftership/services.yaml new file mode 100644 index 00000000000..e4d90646aa6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aftership/services.yaml @@ -0,0 +1,43 @@ +# Describes the format for available aftership services + +add_tracking: + name: Add tracking + description: Add new tracking to Aftership. + fields: + tracking_number: + name: Tracking number + description: Tracking number for the new tracking + required: true + example: "123456789" + selector: + text: + title: + name: Title + description: A custom title for the new tracking + example: "Laptop" + selector: + text: + slug: + name: Slug + description: Slug (carrier) of the new tracking + example: "USPS" + selector: + text: + +remove_tracking: + name: Remove tracking + description: Remove a tracking from Aftership. + fields: + tracking_number: + name: Tracking number + description: Tracking number of the tracking to remove + required: true + example: "123456789" + selector: + text: + slug: + name: Slug + description: Slug (carrier) of the tracking to remove + example: "USPS" + selector: + text: diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/__init__.py new file mode 100644 index 00000000000..5b765da7f8e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/__init__.py @@ -0,0 +1,63 @@ +"""Support for Agent.""" + +from agent import AgentError +from agent.a import Agent + +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CONNECTION, DOMAIN as AGENT_DOMAIN, SERVER_URL + +ATTRIBUTION = "ispyconnect.com" +DEFAULT_BRAND = "Agent DVR by ispyconnect.com" + +FORWARDS = ["alarm_control_panel", "camera"] + + +async def async_setup_entry(hass, config_entry): + """Set up the Agent component.""" + hass.data.setdefault(AGENT_DOMAIN, {}) + + server_origin = config_entry.data[SERVER_URL] + + agent_client = Agent(server_origin, async_get_clientsession(hass)) + try: + await agent_client.update() + except AgentError as err: + await agent_client.close() + raise ConfigEntryNotReady from err + + if not agent_client.is_available: + raise ConfigEntryNotReady + + await agent_client.get_devices() + + hass.data[AGENT_DOMAIN][config_entry.entry_id] = {CONNECTION: agent_client} + + device_registry = await dr.async_get_registry(hass) + + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(AGENT_DOMAIN, agent_client.unique)}, + manufacturer="iSpyConnect", + name=f"Agent {agent_client.name}", + model="Agent DVR", + sw_version=agent_client.version, + ) + + hass.config_entries.async_setup_platforms(config_entry, FORWARDS) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(config_entry, FORWARDS) + + await hass.data[AGENT_DOMAIN][config_entry.entry_id][CONNECTION].close() + + if unload_ok: + hass.data[AGENT_DOMAIN].pop(config_entry.entry_id) + + return unload_ok diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/alarm_control_panel.py b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/alarm_control_panel.py new file mode 100644 index 00000000000..3e093ae46a8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/alarm_control_panel.py @@ -0,0 +1,124 @@ +"""Support for Agent DVR Alarm Control Panels.""" +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, +) + +from .const import CONNECTION, DOMAIN as AGENT_DOMAIN + +ICON = "mdi:security" + +CONF_HOME_MODE_NAME = "home" +CONF_AWAY_MODE_NAME = "away" +CONF_NIGHT_MODE_NAME = "night" + +CONST_ALARM_CONTROL_PANEL_NAME = "Alarm Panel" + + +async def async_setup_entry( + hass, config_entry, async_add_entities, discovery_info=None +): + """Set up the Agent DVR Alarm Control Panels.""" + async_add_entities( + [AgentBaseStation(hass.data[AGENT_DOMAIN][config_entry.entry_id][CONNECTION])] + ) + + +class AgentBaseStation(AlarmControlPanelEntity): + """Representation of an Agent DVR Alarm Control Panel.""" + + def __init__(self, client): + """Initialize the alarm control panel.""" + self._state = None + self._client = client + self._unique_id = f"{client.unique}_CP" + name = CONST_ALARM_CONTROL_PANEL_NAME + self._name = name = f"{client.name} {name}" + + @property + def icon(self): + """Return icon.""" + return ICON + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + + @property + def device_info(self): + """Return the device info for adding the entity to the agent object.""" + return { + "identifiers": {(AGENT_DOMAIN, self._client.unique)}, + "manufacturer": "Agent", + "model": CONST_ALARM_CONTROL_PANEL_NAME, + "sw_version": self._client.version, + } + + async def async_update(self): + """Update the state of the device.""" + await self._client.update() + armed = self._client.is_armed + if armed is None: + self._state = None + return + if armed: + prof = (await self._client.get_active_profile()).lower() + self._state = STATE_ALARM_ARMED_AWAY + if prof == CONF_HOME_MODE_NAME: + self._state = STATE_ALARM_ARMED_HOME + elif prof == CONF_NIGHT_MODE_NAME: + self._state = STATE_ALARM_ARMED_NIGHT + else: + self._state = STATE_ALARM_DISARMED + + async def async_alarm_disarm(self, code=None): + """Send disarm command.""" + await self._client.disarm() + self._state = STATE_ALARM_DISARMED + + async def async_alarm_arm_away(self, code=None): + """Send arm away command. Uses custom mode.""" + await self._client.arm() + await self._client.set_active_profile(CONF_AWAY_MODE_NAME) + self._state = STATE_ALARM_ARMED_AWAY + + async def async_alarm_arm_home(self, code=None): + """Send arm home command. Uses custom mode.""" + await self._client.arm() + await self._client.set_active_profile(CONF_HOME_MODE_NAME) + self._state = STATE_ALARM_ARMED_HOME + + async def async_alarm_arm_night(self, code=None): + """Send arm night command. Uses custom mode.""" + await self._client.arm() + await self._client.set_active_profile(CONF_NIGHT_MODE_NAME) + self._state = STATE_ALARM_ARMED_NIGHT + + @property + def name(self): + """Return the name of the base station.""" + return self._name + + @property + def available(self) -> bool: + """Device available.""" + return self._client.is_available + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._unique_id diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/camera.py b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/camera.py new file mode 100644 index 00000000000..6b2363f50d5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/camera.py @@ -0,0 +1,215 @@ +"""Support for Agent camera streaming.""" +from datetime import timedelta +import logging + +from agent import AgentError + +from homeassistant.components.camera import SUPPORT_ON_OFF +from homeassistant.components.mjpeg.camera import ( + CONF_MJPEG_URL, + CONF_STILL_IMAGE_URL, + MjpegCamera, + filter_urllib3_logging, +) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +from homeassistant.helpers import entity_platform + +from .const import ( + ATTRIBUTION, + CAMERA_SCAN_INTERVAL_SECS, + CONNECTION, + DOMAIN as AGENT_DOMAIN, +) + +SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS) + +_LOGGER = logging.getLogger(__name__) + +_DEV_EN_ALT = "enable_alerts" +_DEV_DS_ALT = "disable_alerts" +_DEV_EN_REC = "start_recording" +_DEV_DS_REC = "stop_recording" +_DEV_SNAP = "snapshot" + +CAMERA_SERVICES = { + _DEV_EN_ALT: "async_enable_alerts", + _DEV_DS_ALT: "async_disable_alerts", + _DEV_EN_REC: "async_start_recording", + _DEV_DS_REC: "async_stop_recording", + _DEV_SNAP: "async_snapshot", +} + + +async def async_setup_entry( + hass, config_entry, async_add_entities, discovery_info=None +): + """Set up the Agent cameras.""" + filter_urllib3_logging() + cameras = [] + + server = hass.data[AGENT_DOMAIN][config_entry.entry_id][CONNECTION] + if not server.devices: + _LOGGER.warning("Could not fetch cameras from Agent server") + return + + for device in server.devices: + if device.typeID == 2: + camera = AgentCamera(device) + cameras.append(camera) + + async_add_entities(cameras) + + platform = entity_platform.async_get_current_platform() + for service, method in CAMERA_SERVICES.items(): + platform.async_register_entity_service(service, {}, method) + + +class AgentCamera(MjpegCamera): + """Representation of an Agent Device Stream.""" + + def __init__(self, device): + """Initialize as a subclass of MjpegCamera.""" + self._servername = device.client.name + self.server_url = device.client._server_url + + device_info = { + CONF_NAME: device.name, + CONF_MJPEG_URL: f"{self.server_url}{device.mjpeg_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", + CONF_STILL_IMAGE_URL: f"{self.server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", + } + self.device = device + self._removed = False + self._name = f"{self._servername} {device.name}" + self._unique_id = f"{device._client.unique}_{device.typeID}_{device.id}" + super().__init__(device_info) + + @property + def device_info(self): + """Return the device info for adding the entity to the agent object.""" + return { + "identifiers": {(AGENT_DOMAIN, self._unique_id)}, + "name": self._name, + "manufacturer": "Agent", + "model": "Camera", + "sw_version": self.device.client.version, + } + + async def async_update(self): + """Update our state from the Agent API.""" + try: + await self.device.update() + if self._removed: + _LOGGER.debug("%s reacquired", self._name) + self._removed = False + except AgentError: + # server still available - camera error + if self.device.client.is_available and not self._removed: + _LOGGER.error("%s lost", self._name) + self._removed = True + + @property + def extra_state_attributes(self): + """Return the Agent DVR camera state attributes.""" + return { + ATTR_ATTRIBUTION: ATTRIBUTION, + "editable": False, + "enabled": self.is_on, + "connected": self.connected, + "detected": self.is_detected, + "alerted": self.is_alerted, + "has_ptz": self.device.has_ptz, + "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.""" + return self.device.recording + + @property + def is_alerted(self) -> bool: + """Return whether the monitor has alerted.""" + return self.device.alerted + + @property + def is_detected(self) -> bool: + """Return whether the monitor has alerted.""" + return self.device.detected + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.device.client.is_available + + @property + def connected(self) -> bool: + """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.""" + return self.device.online + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + if self.is_on: + return "mdi:camcorder" + return "mdi:camcorder-off" + + @property + def motion_detection_enabled(self): + """Return the camera motion detection status.""" + return self.device.detector_active + + @property + def unique_id(self) -> str: + """Return a unique identifier for this agent object.""" + return self._unique_id + + async def async_enable_alerts(self): + """Enable alerts.""" + await self.device.alerts_on() + + async def async_disable_alerts(self): + """Disable alerts.""" + await self.device.alerts_off() + + async def async_enable_motion_detection(self): + """Enable motion detection.""" + await self.device.detector_on() + + async def async_disable_motion_detection(self): + """Disable motion detection.""" + await self.device.detector_off() + + async def async_start_recording(self): + """Start recording.""" + await self.device.record() + + async def async_stop_recording(self): + """Stop recording.""" + await self.device.record_stop() + + async def async_turn_on(self): + """Enable the camera.""" + await self.device.enable() + + async def async_snapshot(self): + """Take a snapshot.""" + await self.device.snapshot() + + async def async_turn_off(self): + """Disable the camera.""" + await self.device.disable() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/config_flow.py new file mode 100644 index 00000000000..a21e6855337 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/config_flow.py @@ -0,0 +1,78 @@ +"""Config flow to configure Agent devices.""" +from agent import AgentConnectionError, AgentError +from agent.a import Agent +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN, SERVER_URL +from .helpers import generate_url + +DEFAULT_PORT = 8090 + + +class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle an Agent config flow.""" + + def __init__(self): + """Initialize the Agent config flow.""" + self.device_config = {} + + async def async_step_user(self, user_input=None): + """Handle an Agent config flow.""" + errors = {} + + if user_input is not None: + host = user_input[CONF_HOST] + port = user_input[CONF_PORT] + + server_origin = generate_url(host, port) + agent_client = Agent(server_origin, async_get_clientsession(self.hass)) + + try: + await agent_client.update() + except AgentConnectionError: + pass + except AgentError: + pass + + await agent_client.close() + + if agent_client.is_available: + await self.async_set_unique_id(agent_client.unique) + + self._abort_if_unique_id_configured( + updates={ + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + SERVER_URL: server_origin, + } + ) + + self.device_config = { + CONF_HOST: host, + CONF_PORT: port, + SERVER_URL: server_origin, + } + + return await self._create_entry(agent_client.name) + + errors["base"] = "cannot_connect" + + data = { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } + + return self.async_show_form( + step_id="user", + description_placeholders=self.device_config, + data_schema=vol.Schema(data), + errors=errors, + ) + + async def _create_entry(self, server_name): + """Create entry for device.""" + return self.async_create_entry(title=server_name, data=self.device_config) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/const.py new file mode 100644 index 00000000000..e571edf9800 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/const.py @@ -0,0 +1,11 @@ +"""Constants for agent_dvr component.""" +DOMAIN = "agent_dvr" +SERVERS = "servers" +DEVICES = "devices" +ENTITIES = "entities" +CAMERA_SCAN_INTERVAL_SECS = 5 +SERVICE_UPDATE = "update" +SIGNAL_UPDATE_AGENT = "agent_update" +ATTRIBUTION = "Data provided by ispyconnect.com" +SERVER_URL = "server_url" +CONNECTION = "connection" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/helpers.py b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/helpers.py new file mode 100644 index 00000000000..028a683946d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/helpers.py @@ -0,0 +1,13 @@ +"""Helpers for Agent DVR component.""" + + +def generate_url(host, port) -> str: + """Create a URL from the host and port.""" + server_origin = host + if "://" not in host: + server_origin = f"http://{host}" + + if server_origin[-1] == "/": + server_origin = server_origin[:-1] + + return f"{server_origin}:{port}/" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/manifest.json new file mode 100644 index 00000000000..7d740bbe731 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "agent_dvr", + "name": "Agent DVR", + "documentation": "https://www.home-assistant.io/integrations/agent_dvr/", + "requirements": ["agent-py==0.0.23"], + "config_flow": true, + "codeowners": ["@ispysoftware"], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/services.yaml b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/services.yaml new file mode 100644 index 00000000000..206b32cb526 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/services.yaml @@ -0,0 +1,39 @@ +start_recording: + name: Start recording + description: Enable continuous recording. + target: + entity: + integration: agent_dvr + domain: camera + +stop_recording: + name: Stop recording + description: Disable continuous recording. + target: + entity: + integration: agent_dvr + domain: camera + +enable_alerts: + name: Enable alerts + description: Enable alerts + target: + entity: + integration: agent_dvr + domain: camera + +disable_alerts: + name: Disable alerts + description: Disable alerts + target: + entity: + integration: agent_dvr + domain: camera + +snapshot: + name: Snapshot + description: Take a photo + target: + entity: + integration: agent_dvr + domain: camera diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/strings.json new file mode 100644 index 00000000000..127fbb69b33 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "title": "Set up Agent DVR", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/ca.json new file mode 100644 index 00000000000..36f97cfd53e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port" + }, + "title": "Configuraci\u00f3 de Agent DVR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/cs.json new file mode 100644 index 00000000000..8b5c6a4a2aa --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + }, + "title": "Nastaven\u00ed Agent DVR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/de.json new file mode 100644 index 00000000000..10a8307ada1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Richten Sie den Agent DVR ein" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/en.json new file mode 100644 index 00000000000..b295a465600 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "already_in_progress": "Configuration flow is already in progress", + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Set up Agent DVR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/es-419.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/es-419.json new file mode 100644 index 00000000000..63be2ce403a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/es-419.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ya est\u00e1 en progreso." + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Puerto" + }, + "title": "Configurar Agent DVR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/es.json new file mode 100644 index 00000000000..c1771d7e80b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Puerto" + }, + "title": "Configurar el Agente de DVR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/et.json new file mode 100644 index 00000000000..128f446d1a1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "host": "", + "port": "" + }, + "title": "Seadista Agent DVR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/fi.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/fi.json new file mode 100644 index 00000000000..94b3d7c4d6a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/fi.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Laite on jo m\u00e4\u00e4ritetty" + }, + "error": { + "already_in_progress": "Laitteen m\u00e4\u00e4ritysvirta on jo k\u00e4ynniss\u00e4." + }, + "step": { + "user": { + "data": { + "host": "Palvelin", + "port": "Portti" + }, + "title": "Asenna Agent DVR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/fr.json new file mode 100644 index 00000000000..e78c1da7d8b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "already_in_progress": "La configuration de l'appareil est d\u00e9j\u00e0 en cours.", + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "user": { + "data": { + "host": "Nom d'h\u00f4te ou adresse IP", + "port": "Port" + }, + "title": "Configurer l'agent DVR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/he.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/he.json new file mode 100644 index 00000000000..6268822a90a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u05d4\u05de\u05db\u05e9\u05d9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "\u05e4\u05d5\u05e8\u05d8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/hu.json new file mode 100644 index 00000000000..49968ceea75 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "Hoszt", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/id.json new file mode 100644 index 00000000000..f2ee1cc6622 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Siapkan Agent DVR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/it.json new file mode 100644 index 00000000000..8c33cfcc63b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta" + }, + "title": "Configurare Agent DVR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/ka.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/ka.json new file mode 100644 index 00000000000..fa4c9c0abd3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/ko.json new file mode 100644 index 00000000000..add0b917100 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8" + }, + "title": "Agent DVR \uc124\uc815\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/lb.json new file mode 100644 index 00000000000..ec552e607f4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/lb.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "already_in_progress": "Konfiguratioun's Oflaf ass schonn am gaangen.", + "cannot_connect": "Feeler beim verbannen" + }, + "step": { + "user": { + "data": { + "host": "Apparat", + "port": "Port" + }, + "title": "Agent DVR ariichten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/nl.json new file mode 100644 index 00000000000..7c679f66c11 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "already_in_progress": "De configuratiestroom is al aan de gang", + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Poort" + }, + "title": "Stel Agent DVR in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/no.json new file mode 100644 index 00000000000..efe83b8436f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "port": "Port" + }, + "title": "Konfigurere Agent DVR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/pl.json new file mode 100644 index 00000000000..fc97fdfa85a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port" + }, + "title": "Konfiguracja Agent DVR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/pt-BR.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/pt-BR.json new file mode 100644 index 00000000000..0077ceddd46 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/pt-BR.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/pt.json new file mode 100644 index 00000000000..f1ef5ef665f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/ru.json new file mode 100644 index 00000000000..7c76fa87813 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/ru.json @@ -0,0 +1,20 @@ +{ + "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": { + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "Agent DVR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/sv.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/sv.json new file mode 100644 index 00000000000..cf600b98e96 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "already_in_progress": "Konfigurationsfl\u00f6de f\u00f6r enhet p\u00e5g\u00e5r redan." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + }, + "title": "Konfigurera DVR Agent" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/tr.json new file mode 100644 index 00000000000..31dddab7795 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + }, + "title": "Agent DVR'\u0131 kurun" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/uk.json new file mode 100644 index 00000000000..fef8d45d5a4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "Agent DVR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/zh-Hans.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/zh-Hans.json new file mode 100644 index 00000000000..2941dfd9383 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/zh-Hant.json new file mode 100644 index 00000000000..9f5e123008a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/agent_dvr/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + }, + "title": "\u8a2d\u5b9a Agent DVR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/air_quality/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/air_quality/__init__.py new file mode 100644 index 00000000000..1b8ab5f9c30 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/air_quality/__init__.py @@ -0,0 +1,163 @@ +"""Component for handling Air Quality data for your location.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Final, final + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.config_validation import ( # noqa: F401 + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.typing import ConfigType, StateType + +_LOGGER: Final = logging.getLogger(__name__) + +ATTR_AQI: Final = "air_quality_index" +ATTR_CO2: Final = "carbon_dioxide" +ATTR_CO: Final = "carbon_monoxide" +ATTR_N2O: Final = "nitrogen_oxide" +ATTR_NO: Final = "nitrogen_monoxide" +ATTR_NO2: Final = "nitrogen_dioxide" +ATTR_OZONE: Final = "ozone" +ATTR_PM_0_1: Final = "particulate_matter_0_1" +ATTR_PM_10: Final = "particulate_matter_10" +ATTR_PM_2_5: Final = "particulate_matter_2_5" +ATTR_SO2: Final = "sulphur_dioxide" + +DOMAIN: Final = "air_quality" + +ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" + +SCAN_INTERVAL: Final = timedelta(seconds=30) + +PROP_TO_ATTR: Final[dict[str, str]] = { + "air_quality_index": ATTR_AQI, + "attribution": ATTR_ATTRIBUTION, + "carbon_dioxide": ATTR_CO2, + "carbon_monoxide": ATTR_CO, + "nitrogen_oxide": ATTR_N2O, + "nitrogen_monoxide": ATTR_NO, + "nitrogen_dioxide": ATTR_NO2, + "ozone": ATTR_OZONE, + "particulate_matter_0_1": ATTR_PM_0_1, + "particulate_matter_10": ATTR_PM_10, + "particulate_matter_2_5": ATTR_PM_2_5, + "sulphur_dioxide": ATTR_SO2, +} + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the air quality component.""" + component = hass.data[DOMAIN] = EntityComponent( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL + ) + await component.async_setup(config) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) + + +class AirQualityEntity(Entity): + """ABC for air quality data.""" + + @property + def particulate_matter_2_5(self) -> StateType: + """Return the particulate matter 2.5 level.""" + raise NotImplementedError() + + @property + def particulate_matter_10(self) -> StateType: + """Return the particulate matter 10 level.""" + return None + + @property + def particulate_matter_0_1(self) -> StateType: + """Return the particulate matter 0.1 level.""" + return None + + @property + def air_quality_index(self) -> StateType: + """Return the Air Quality Index (AQI).""" + return None + + @property + def ozone(self) -> StateType: + """Return the O3 (ozone) level.""" + return None + + @property + def carbon_monoxide(self) -> StateType: + """Return the CO (carbon monoxide) level.""" + return None + + @property + def carbon_dioxide(self) -> StateType: + """Return the CO2 (carbon dioxide) level.""" + return None + + @property + def attribution(self) -> StateType: + """Return the attribution.""" + return None + + @property + def sulphur_dioxide(self) -> StateType: + """Return the SO2 (sulphur dioxide) level.""" + return None + + @property + def nitrogen_oxide(self) -> StateType: + """Return the N2O (nitrogen oxide) level.""" + return None + + @property + def nitrogen_monoxide(self) -> StateType: + """Return the NO (nitrogen monoxide) level.""" + return None + + @property + def nitrogen_dioxide(self) -> StateType: + """Return the NO2 (nitrogen dioxide) level.""" + return None + + @final + @property + def state_attributes(self) -> dict[str, str | int | float]: + """Return the state attributes.""" + data: dict[str, str | int | float] = {} + + for prop, attr in PROP_TO_ATTR.items(): + value = getattr(self, prop) + if value is not None: + data[attr] = value + + return data + + @property + def state(self) -> StateType: + """Return the current state.""" + return self.particulate_matter_2_5 + + @property + def unit_of_measurement(self) -> str: + """Return the unit of measurement of this entity.""" + return CONCENTRATION_MICROGRAMS_PER_CUBIC_METER diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/air_quality/group.py b/homeassistant-2021.6.0.dev0/homeassistant/components/air_quality/group.py new file mode 100644 index 00000000000..2ac081496cd --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/air_quality/group.py @@ -0,0 +1,13 @@ +"""Describe group states.""" + + +from homeassistant.components.group import GroupIntegrationRegistry +from homeassistant.core import HomeAssistant, callback + + +@callback +def async_describe_on_off_states( + hass: HomeAssistant, registry: GroupIntegrationRegistry +) -> None: + """Describe group on off states.""" + registry.exclude_domain() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/air_quality/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/air_quality/manifest.json new file mode 100644 index 00000000000..55fbdbdafd1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/air_quality/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "air_quality", + "name": "Air Quality", + "documentation": "https://www.home-assistant.io/integrations/air_quality", + "codeowners": [], + "quality_scale": "internal" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/__init__.py new file mode 100644 index 00000000000..26e14a7642e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/__init__.py @@ -0,0 +1,194 @@ +"""The Airly integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from math import ceil + +from aiohttp import ClientSession +from aiohttp.client_exceptions import ClientConnectorError +from airly import Airly +from airly.exceptions import AirlyError +import async_timeout + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import async_get_registry +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt as dt_util + +from .const import ( + ATTR_API_ADVICE, + ATTR_API_CAQI, + ATTR_API_CAQI_DESCRIPTION, + ATTR_API_CAQI_LEVEL, + CONF_USE_NEAREST, + DOMAIN, + MAX_UPDATE_INTERVAL, + MIN_UPDATE_INTERVAL, + NO_AIRLY_SENSORS, +) + +PLATFORMS = ["air_quality", "sensor"] + +_LOGGER = logging.getLogger(__name__) + + +def set_update_interval(instances_count: int, requests_remaining: int) -> timedelta: + """ + Return data update interval. + + The number of requests is reset at midnight UTC so we calculate the update + interval based on number of minutes until midnight, the number of Airly instances + and the number of remaining requests. + """ + now = dt_util.utcnow() + midnight = dt_util.find_next_time_expression_time( + now, seconds=[0], minutes=[0], hours=[0] + ) + minutes_to_midnight = (midnight - now).total_seconds() / 60 + interval = timedelta( + minutes=min( + max( + ceil(minutes_to_midnight / requests_remaining * instances_count), + MIN_UPDATE_INTERVAL, + ), + MAX_UPDATE_INTERVAL, + ) + ) + + _LOGGER.debug("Data will be update every %s", interval) + + return interval + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Airly as config entry.""" + api_key = entry.data[CONF_API_KEY] + latitude = entry.data[CONF_LATITUDE] + longitude = entry.data[CONF_LONGITUDE] + use_nearest = entry.data.get(CONF_USE_NEAREST, False) + + # For backwards compat, set unique ID + if entry.unique_id is None: + hass.config_entries.async_update_entry( + entry, unique_id=f"{latitude}-{longitude}" + ) + + # identifiers in device_info should use tuple[str, str] type, but latitude and + # longitude are float, so we convert old device entries to use correct types + # We used to use a str 3-tuple here sometime, convert that to a 2-tuple too. + device_registry = await async_get_registry(hass) + old_ids = (DOMAIN, latitude, longitude) + for old_ids in ( + (DOMAIN, latitude, longitude), + ( + DOMAIN, + str(latitude), + str(longitude), + ), + ): + device_entry = device_registry.async_get_device({old_ids}) # type: ignore[arg-type] + if device_entry and entry.entry_id in device_entry.config_entries: + new_ids = (DOMAIN, f"{latitude}-{longitude}") + device_registry.async_update_device( + device_entry.id, new_identifiers={new_ids} + ) + + websession = async_get_clientsession(hass) + + update_interval = timedelta(minutes=MIN_UPDATE_INTERVAL) + + coordinator = AirlyDataUpdateCoordinator( + hass, websession, api_key, latitude, longitude, update_interval, use_nearest + ) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + 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 AirlyDataUpdateCoordinator(DataUpdateCoordinator): + """Define an object to hold Airly data.""" + + def __init__( + self, + hass: HomeAssistant, + session: ClientSession, + api_key: str, + latitude: float, + longitude: float, + update_interval: timedelta, + use_nearest: bool, + ): + """Initialize.""" + self.latitude = latitude + self.longitude = longitude + self.airly = Airly(api_key, session) + self.use_nearest = use_nearest + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + + async def _async_update_data(self) -> dict[str, str | float | int]: + """Update data via library.""" + data: dict[str, str | float | int] = {} + if self.use_nearest: + measurements = self.airly.create_measurements_session_nearest( + self.latitude, self.longitude, max_distance_km=5 + ) + else: + measurements = self.airly.create_measurements_session_point( + self.latitude, self.longitude + ) + with async_timeout.timeout(20): + try: + await measurements.update() + except (AirlyError, ClientConnectorError) as error: + raise UpdateFailed(error) from error + + _LOGGER.debug( + "Requests remaining: %s/%s", + self.airly.requests_remaining, + self.airly.requests_per_day, + ) + + # Airly API sometimes returns None for requests remaining so we update + # update_interval only if we have valid value. + if self.airly.requests_remaining: + self.update_interval = set_update_interval( + len(self.hass.config_entries.async_entries(DOMAIN)), + self.airly.requests_remaining, + ) + + values = measurements.current["values"] + index = measurements.current["indexes"][0] + standards = measurements.current["standards"] + + if index["description"] == NO_AIRLY_SENSORS: + raise UpdateFailed("Can't retrieve data: no Airly sensors in this area") + for value in values: + data[value["name"]] = value["value"] + for standard in standards: + data[f"{standard['pollutant']}_LIMIT"] = standard["limit"] + data[f"{standard['pollutant']}_PERCENT"] = standard["percent"] + data[ATTR_API_CAQI] = index["value"] + data[ATTR_API_CAQI_LEVEL] = index["level"].lower().replace("_", " ") + data[ATTR_API_CAQI_DESCRIPTION] = index["description"] + data[ATTR_API_ADVICE] = index["advice"] + return data diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/air_quality.py b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/air_quality.py new file mode 100644 index 00000000000..337d3a723fa --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/air_quality.py @@ -0,0 +1,143 @@ +"""Support for the Airly air_quality service.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.air_quality import ( + ATTR_AQI, + ATTR_PM_2_5, + ATTR_PM_10, + AirQualityEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import AirlyDataUpdateCoordinator +from .const import ( + ATTR_API_ADVICE, + ATTR_API_CAQI, + ATTR_API_CAQI_DESCRIPTION, + ATTR_API_CAQI_LEVEL, + ATTR_API_PM10, + ATTR_API_PM10_LIMIT, + ATTR_API_PM10_PERCENT, + ATTR_API_PM25, + ATTR_API_PM25_LIMIT, + ATTR_API_PM25_PERCENT, + ATTRIBUTION, + DEFAULT_NAME, + DOMAIN, + LABEL_ADVICE, + MANUFACTURER, +) + +LABEL_AQI_DESCRIPTION = f"{ATTR_AQI}_description" +LABEL_AQI_LEVEL = f"{ATTR_AQI}_level" +LABEL_PM_2_5_LIMIT = f"{ATTR_PM_2_5}_limit" +LABEL_PM_2_5_PERCENT = f"{ATTR_PM_2_5}_percent_of_limit" +LABEL_PM_10_LIMIT = f"{ATTR_PM_10}_limit" +LABEL_PM_10_PERCENT = f"{ATTR_PM_10}_percent_of_limit" + +PARALLEL_UPDATES = 1 + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Airly air_quality entity based on a config entry.""" + name = entry.data[CONF_NAME] + + coordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities([AirlyAirQuality(coordinator, name)], False) + + +class AirlyAirQuality(CoordinatorEntity, AirQualityEntity): + """Define an Airly air quality.""" + + coordinator: AirlyDataUpdateCoordinator + + def __init__(self, coordinator: AirlyDataUpdateCoordinator, name: str) -> None: + """Initialize.""" + super().__init__(coordinator) + self._name = name + self._icon = "mdi:blur" + + @property + def name(self) -> str: + """Return the name.""" + return self._name + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def air_quality_index(self) -> float | None: + """Return the air quality index.""" + return round_state(self.coordinator.data[ATTR_API_CAQI]) + + @property + def particulate_matter_2_5(self) -> float | None: + """Return the particulate matter 2.5 level.""" + return round_state(self.coordinator.data.get(ATTR_API_PM25)) + + @property + def particulate_matter_10(self) -> float | None: + """Return the particulate matter 10 level.""" + return round_state(self.coordinator.data.get(ATTR_API_PM10)) + + @property + def attribution(self) -> str: + """Return the attribution.""" + return ATTRIBUTION + + @property + def unique_id(self) -> str: + """Return a unique_id for this entity.""" + return f"{self.coordinator.latitude}-{self.coordinator.longitude}" + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return { + "identifiers": { + ( + DOMAIN, + f"{self.coordinator.latitude}-{self.coordinator.longitude}", + ) + }, + "name": DEFAULT_NAME, + "manufacturer": MANUFACTURER, + "entry_type": "service", + } + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes.""" + attrs = { + LABEL_AQI_DESCRIPTION: self.coordinator.data[ATTR_API_CAQI_DESCRIPTION], + LABEL_ADVICE: self.coordinator.data[ATTR_API_ADVICE], + LABEL_AQI_LEVEL: self.coordinator.data[ATTR_API_CAQI_LEVEL], + } + if ATTR_API_PM25 in self.coordinator.data: + attrs[LABEL_PM_2_5_LIMIT] = self.coordinator.data[ATTR_API_PM25_LIMIT] + attrs[LABEL_PM_2_5_PERCENT] = round( + self.coordinator.data[ATTR_API_PM25_PERCENT] + ) + if ATTR_API_PM10 in self.coordinator.data: + attrs[LABEL_PM_10_LIMIT] = self.coordinator.data[ATTR_API_PM10_LIMIT] + attrs[LABEL_PM_10_PERCENT] = round( + self.coordinator.data[ATTR_API_PM10_PERCENT] + ) + return attrs + + +def round_state(state: float | None) -> float | None: + """Round state.""" + return round(state) if state else state diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/config_flow.py new file mode 100644 index 00000000000..598aa15b9b6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/config_flow.py @@ -0,0 +1,119 @@ +"""Adds config flow for Airly.""" +from __future__ import annotations + +from typing import Any + +from aiohttp import ClientSession +from airly import Airly +from airly.exceptions import AirlyError +import async_timeout +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + HTTP_NOT_FOUND, + HTTP_UNAUTHORIZED, +) +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +from .const import CONF_USE_NEAREST, DOMAIN, NO_AIRLY_SENSORS + + +class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for Airly.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + errors = {} + use_nearest = False + + websession = async_get_clientsession(self.hass) + + if user_input is not None: + await self.async_set_unique_id( + f"{user_input[CONF_LATITUDE]}-{user_input[CONF_LONGITUDE]}" + ) + self._abort_if_unique_id_configured() + try: + location_point_valid = await test_location( + websession, + user_input["api_key"], + user_input["latitude"], + user_input["longitude"], + ) + if not location_point_valid: + await test_location( + websession, + user_input["api_key"], + user_input["latitude"], + user_input["longitude"], + use_nearest=True, + ) + except AirlyError as err: + if err.status_code == HTTP_UNAUTHORIZED: + errors["base"] = "invalid_api_key" + if err.status_code == HTTP_NOT_FOUND: + errors["base"] = "wrong_location" + else: + if not location_point_valid: + use_nearest = True + return self.async_create_entry( + title=user_input[CONF_NAME], + data={**user_input, CONF_USE_NEAREST: use_nearest}, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_API_KEY): str, + vol.Optional( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Optional( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + vol.Optional( + CONF_NAME, default=self.hass.config.location_name + ): str, + } + ), + errors=errors, + ) + + +async def test_location( + client: ClientSession, + api_key: str, + latitude: float, + longitude: float, + use_nearest: bool = False, +) -> bool: + """Return true if location is valid.""" + airly = Airly(api_key, client) + if use_nearest: + measurements = airly.create_measurements_session_nearest( + latitude=latitude, longitude=longitude, max_distance_km=5 + ) + else: + measurements = airly.create_measurements_session_point( + latitude=latitude, longitude=longitude + ) + with async_timeout.timeout(10): + await measurements.update() + + current = measurements.current + + if current["indexes"][0]["description"] == NO_AIRLY_SENSORS: + return False + return True diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/const.py new file mode 100644 index 00000000000..5136f54d6f2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/const.py @@ -0,0 +1,68 @@ +"""Constants for Airly integration.""" +from __future__ import annotations + +from typing import Final + +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, + PRESSURE_HPA, + TEMP_CELSIUS, +) + +from .model import SensorDescription + +ATTR_API_ADVICE: Final = "ADVICE" +ATTR_API_CAQI: Final = "CAQI" +ATTR_API_CAQI_DESCRIPTION: Final = "DESCRIPTION" +ATTR_API_CAQI_LEVEL: Final = "LEVEL" +ATTR_API_HUMIDITY: Final = "HUMIDITY" +ATTR_API_PM1: Final = "PM1" +ATTR_API_PM10: Final = "PM10" +ATTR_API_PM10_LIMIT: Final = "PM10_LIMIT" +ATTR_API_PM10_PERCENT: Final = "PM10_PERCENT" +ATTR_API_PM25: Final = "PM25" +ATTR_API_PM25_LIMIT: Final = "PM25_LIMIT" +ATTR_API_PM25_PERCENT: Final = "PM25_PERCENT" +ATTR_API_PRESSURE: Final = "PRESSURE" +ATTR_API_TEMPERATURE: Final = "TEMPERATURE" + +ATTRIBUTION: Final = "Data provided by Airly" +CONF_USE_NEAREST: Final = "use_nearest" +DEFAULT_NAME: Final = "Airly" +DOMAIN: Final = "airly" +LABEL_ADVICE: Final = "advice" +MANUFACTURER: Final = "Airly sp. z o.o." +MAX_UPDATE_INTERVAL: Final = 90 +MIN_UPDATE_INTERVAL: Final = 5 +NO_AIRLY_SENSORS: Final = "There are no Airly sensors in this area yet." + +SENSOR_TYPES: dict[str, SensorDescription] = { + ATTR_API_PM1: { + "device_class": None, + "icon": "mdi:blur", + "label": ATTR_API_PM1, + "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + }, + ATTR_API_HUMIDITY: { + "device_class": DEVICE_CLASS_HUMIDITY, + "icon": None, + "label": ATTR_API_HUMIDITY.capitalize(), + "unit": PERCENTAGE, + }, + ATTR_API_PRESSURE: { + "device_class": DEVICE_CLASS_PRESSURE, + "icon": None, + "label": ATTR_API_PRESSURE.capitalize(), + "unit": PRESSURE_HPA, + }, + ATTR_API_TEMPERATURE: { + "device_class": DEVICE_CLASS_TEMPERATURE, + "icon": None, + "label": ATTR_API_TEMPERATURE.capitalize(), + "unit": TEMP_CELSIUS, + }, +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/manifest.json new file mode 100644 index 00000000000..430e51c6e9e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "airly", + "name": "Airly", + "documentation": "https://www.home-assistant.io/integrations/airly", + "codeowners": ["@bieniu"], + "requirements": ["airly==1.1.0"], + "config_flow": true, + "quality_scale": "platinum", + "iot_class": "cloud_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/model.py b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/model.py new file mode 100644 index 00000000000..42091d449e3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/model.py @@ -0,0 +1,13 @@ +"""Type definitions for Airly integration.""" +from __future__ import annotations + +from typing import TypedDict + + +class SensorDescription(TypedDict): + """Sensor description class.""" + + device_class: str | None + icon: str | None + label: str + unit: str diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/sensor.py new file mode 100644 index 00000000000..b978afb25a9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/sensor.py @@ -0,0 +1,114 @@ +"""Support for the Airly sensor service.""" +from __future__ import annotations + +from typing import Any, cast + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import AirlyDataUpdateCoordinator +from .const import ( + ATTR_API_PM1, + ATTR_API_PRESSURE, + ATTRIBUTION, + DEFAULT_NAME, + DOMAIN, + MANUFACTURER, + SENSOR_TYPES, +) + +PARALLEL_UPDATES = 1 + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Airly sensor entities based on a config entry.""" + name = entry.data[CONF_NAME] + + coordinator = hass.data[DOMAIN][entry.entry_id] + + sensors = [] + for sensor in SENSOR_TYPES: + # When we use the nearest method, we are not sure which sensors are available + if coordinator.data.get(sensor): + sensors.append(AirlySensor(coordinator, name, sensor)) + + async_add_entities(sensors, False) + + +class AirlySensor(CoordinatorEntity, SensorEntity): + """Define an Airly sensor.""" + + coordinator: AirlyDataUpdateCoordinator + + def __init__( + self, coordinator: AirlyDataUpdateCoordinator, name: str, kind: str + ) -> None: + """Initialize.""" + super().__init__(coordinator) + self._name = name + self._description = SENSOR_TYPES[kind] + self.kind = kind + self._state = None + self._unit_of_measurement = None + self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def name(self) -> str: + """Return the name.""" + return f"{self._name} {self._description['label']}" + + @property + def state(self) -> StateType: + """Return the state.""" + self._state = self.coordinator.data[self.kind] + if self.kind in [ATTR_API_PM1, ATTR_API_PRESSURE]: + return round(cast(float, self._state)) + return round(cast(float, self._state), 1) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes.""" + return self._attrs + + @property + def icon(self) -> str | None: + """Return the icon.""" + return self._description["icon"] + + @property + def device_class(self) -> str | None: + """Return the device_class.""" + return self._description["device_class"] + + @property + def unique_id(self) -> str: + """Return a unique_id for this entity.""" + return f"{self.coordinator.latitude}-{self.coordinator.longitude}-{self.kind.lower()}" + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return { + "identifiers": { + ( + DOMAIN, + f"{self.coordinator.latitude}-{self.coordinator.longitude}", + ) + }, + "name": DEFAULT_NAME, + "manufacturer": MANUFACTURER, + "entry_type": "service", + } + + @property + def unit_of_measurement(self) -> str | None: + """Return the unit the value is expressed in.""" + return self._description["unit"] diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/strings.json new file mode 100644 index 00000000000..c6b6f1e6a41 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/strings.json @@ -0,0 +1,30 @@ +{ + "config": { + "step": { + "user": { + "title": "Airly", + "description": "Set up Airly air quality integration. To generate API key go to https://developer.airly.eu/register", + "data": { + "name": "[%key:common::config_flow::data::name%]", + "api_key": "[%key:common::config_flow::data::api_key%]", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]" + } + } + }, + "error": { + "wrong_location": "No Airly measuring stations in this area.", + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + } + }, + "system_health": { + "info": { + "can_reach_server": "Reach Airly server", + "requests_remaining": "Remaining allowed requests", + "requests_per_day": "Allowed requests per day" + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/system_health.py b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/system_health.py new file mode 100644 index 00000000000..b1f6bc36c91 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/system_health.py @@ -0,0 +1,33 @@ +"""Provide info to system health.""" +from __future__ import annotations + +from typing import Any + +from airly import Airly + +from homeassistant.components import system_health +from homeassistant.core import HomeAssistant, callback + +from .const import DOMAIN + + +@callback +def async_register( + hass: HomeAssistant, register: system_health.SystemHealthRegistration +) -> None: + """Register system health callbacks.""" + register.async_register_info(system_health_info) + + +async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: + """Get info for the info page.""" + requests_remaining = list(hass.data[DOMAIN].values())[0].airly.requests_remaining + requests_per_day = list(hass.data[DOMAIN].values())[0].airly.requests_per_day + + return { + "can_reach_server": system_health.async_check_can_reach_url( + hass, Airly.AIRLY_API_URL + ), + "requests_remaining": requests_remaining, + "requests_per_day": requests_per_day, + } diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/bg.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/bg.json new file mode 100644 index 00000000000..f0836c8e5ed --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/bg.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "wrong_location": "\u0412 \u0442\u0430\u0437\u0438 \u043e\u0431\u043b\u0430\u0441\u0442 \u043d\u044f\u043c\u0430 \u0438\u0437\u043c\u0435\u0440\u0432\u0430\u0442\u0435\u043b\u043d\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 Airly." + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447 \u0437\u0430 Airly", + "latitude": "\u0428\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0414\u044a\u043b\u0436\u0438\u043d\u0430", + "name": "\u0418\u043c\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0430 \u0432\u044a\u0437\u0434\u0443\u0445\u0430 Airly \u0417\u0430 \u0434\u0430 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u0442\u0435 \u043a\u043b\u044e\u0447 \u0437\u0430 API, \u043e\u0442\u0438\u0434\u0435\u0442\u0435 \u043d\u0430 https://developer.airly.eu/register", + "title": "Airly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/ca.json new file mode 100644 index 00000000000..e76cec94f4c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/ca.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada" + }, + "error": { + "invalid_api_key": "Clau API inv\u00e0lida", + "wrong_location": "No hi ha estacions de mesura Airly en aquesta zona." + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nom" + }, + "description": "Configura una integraci\u00f3 de qualitat d'aire Airly. Per generar la clau API, v\u00e9s a https://developer.airly.eu/register", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Servidor d'Airly accessible", + "requests_per_day": "Sol\u00b7licituds per dia permeses", + "requests_remaining": "Sol\u00b7licituds permeses restants" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/cs.json new file mode 100644 index 00000000000..8b35399bcb0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/cs.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno" + }, + "error": { + "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API", + "wrong_location": "\u017d\u00e1dn\u00e9 m\u011b\u0159ic\u00ed stanice Airly v t\u00e9to oblasti." + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", + "name": "Jm\u00e9no" + }, + "description": "Nastavte integraci kvality vzduchu Airly. Chcete-li vygenerovat kl\u00ed\u010d rozhran\u00ed API, p\u0159ejd\u011bte na https://developer.airly.eu/register", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Lze kontaktovat Airly server" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/da.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/da.json new file mode 100644 index 00000000000..53b05a38992 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/da.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Airly-integration for disse koordinater er allerede konfigureret." + }, + "error": { + "wrong_location": "Ingen Airly-m\u00e5lestationer i dette omr\u00e5de." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API-n\u00f8gle", + "latitude": "Breddegrad", + "longitude": "L\u00e6ngdegrad", + "name": "Integrationens navn" + }, + "description": "Konfigurer Airly luftkvalitetsintegration. For at generere API-n\u00f8gle, g\u00e5 til https://developer.airly.eu/register", + "title": "Airly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/de.json new file mode 100644 index 00000000000..b13798c0ae3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/de.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Standort ist bereits konfiguriert" + }, + "error": { + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", + "wrong_location": "Keine Airly Luftmessstation an diesem Ort" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "name": "Name" + }, + "description": "Einrichtung der Airly-Luftqualit\u00e4t Integration. Um einen API-Schl\u00fcssel zu generieren, registriere dich auf https://developer.airly.eu/register", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Airly-Server erreichen", + "requests_per_day": "Erlaubte Anfragen pro Tag", + "requests_remaining": "Verbleibende erlaubte Anfragen" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/el.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/el.json new file mode 100644 index 00000000000..e39b0aef88d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/el.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_api_key": "\u0386\u03ba\u03c5\u03c1\u03bf API \u03ba\u03bb\u03b5\u03b9\u03b4\u03af" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/en.json new file mode 100644 index 00000000000..0a5426c87d8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/en.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Location is already configured" + }, + "error": { + "invalid_api_key": "Invalid API key", + "wrong_location": "No Airly measuring stations in this area." + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name" + }, + "description": "Set up Airly air quality integration. To generate API key go to https://developer.airly.eu/register", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Reach Airly server", + "requests_per_day": "Allowed requests per day", + "requests_remaining": "Remaining allowed requests" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/es-419.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/es-419.json new file mode 100644 index 00000000000..c7d1e388d67 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/es-419.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "La integraci\u00f3n a\u00e9rea para estas coordenadas ya est\u00e1 configurada." + }, + "error": { + "wrong_location": "No hay estaciones de medici\u00f3n Airly en esta \u00e1rea." + }, + "step": { + "user": { + "data": { + "api_key": "Clave API de Airly", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre de la integraci\u00f3n" + }, + "description": "Configure la integraci\u00f3n de la calidad del aire de Airly. Para generar la clave API, vaya a https://developer.airly.eu/register", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "requests_per_day": "Solicitudes permitidas por d\u00eda", + "requests_remaining": "Solicitudes permitidas restantes" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/es.json new file mode 100644 index 00000000000..a96a2f62293 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/es.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada" + }, + "error": { + "invalid_api_key": "Clave API no v\u00e1lida", + "wrong_location": "No hay estaciones de medici\u00f3n Airly en esta zona." + }, + "step": { + "user": { + "data": { + "api_key": "Clave API", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre" + }, + "description": "Establecer la integraci\u00f3n de la calidad del aire de Airly. Para generar la clave de la API vaya a https://developer.airly.eu/register", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Alcanzar el servidor Airly", + "requests_per_day": "Solicitudes permitidas por d\u00eda", + "requests_remaining": "Solicitudes permitidas restantes" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/et.json new file mode 100644 index 00000000000..6730e131ac2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/et.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Asukoht on juba m\u00e4\u00e4ratud" + }, + "error": { + "invalid_api_key": "Vigane API v\u00f5ti", + "wrong_location": "Selles piirkonnas pole Airly m\u00f5\u00f5tejaamu." + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad", + "name": "Nimi" + }, + "description": "Seadista Airly \u00f5hukvaliteedi andmete sidumine. API v\u00f5tme loomiseks mine aadressile https://developer.airly.eu/register", + "title": "" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u00dchendus Airly serveriga", + "requests_per_day": "Lubatud p\u00e4ringuid p\u00e4evas", + "requests_remaining": "Lubatud p\u00e4ringute arv" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/fr.json new file mode 100644 index 00000000000..a23f455e0b8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/fr.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "L'int\u00e9gration des coordonn\u00e9es d'Airly est d\u00e9j\u00e0 configur\u00e9." + }, + "error": { + "invalid_api_key": "Cl\u00e9 API invalide", + "wrong_location": "Aucune station de mesure Airly dans cette zone." + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nom de l'int\u00e9gration" + }, + "description": "Configurez l'int\u00e9gration de la qualit\u00e9 de l'air Airly. Pour g\u00e9n\u00e9rer une cl\u00e9 API, rendez-vous sur https://developer.airly.eu/register.", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Acc\u00e8s au serveur Airly", + "requests_per_day": "Demandes autoris\u00e9es par jour", + "requests_remaining": "Demandes autoris\u00e9es restantes" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/he.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/he.json new file mode 100644 index 00000000000..4c49313d977 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/hu.json new file mode 100644 index 00000000000..b9fd0c9e05c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/hu.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", + "wrong_location": "Ezen a ter\u00fcleten nincs Airly m\u00e9r\u0151\u00e1llom\u00e1s." + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "name": "N\u00e9v" + }, + "description": "Az Airly leveg\u0151min\u0151s\u00e9gi integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Api-kulcs l\u00e9trehoz\u00e1s\u00e1hoz nyissa meg a k\u00f6vetkez\u0151 weboldalt: https://developer.airly.eu/register", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "requests_per_day": "Enged\u00e9lyezett k\u00e9r\u00e9sek naponta", + "requests_remaining": "Fennmarad\u00f3 enged\u00e9lyezett k\u00e9r\u00e9sek" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/id.json new file mode 100644 index 00000000000..57b4c0d95f9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/id.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Lokasi sudah dikonfigurasi" + }, + "error": { + "invalid_api_key": "Kunci API tidak valid", + "wrong_location": "Tidak ada stasiun pengukur Airly di daerah ini." + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "latitude": "Lintang", + "longitude": "Bujur", + "name": "Nama" + }, + "description": "Siapkan integrasi kualitas udara Airly. Untuk membuat kunci API, buka https://developer.airly.eu/register", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Keterjangkauan server Airly", + "requests_per_day": "Permintaan yang diizinkan per hari", + "requests_remaining": "Sisa permintaan yang diizinkan" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/it.json new file mode 100644 index 00000000000..385b8117437 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/it.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "La posizione \u00e8 gi\u00e0 configurata" + }, + "error": { + "invalid_api_key": "Chiave API non valida", + "wrong_location": "Nessuna stazione di misurazione Airly in quest'area." + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "latitude": "Latitudine", + "longitude": "Logitudine", + "name": "Nome" + }, + "description": "Configurazione dell'integrazione della qualit\u00e0 dell'aria Airly. Per generare la chiave API andare su https://developer.airly.eu/register", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Raggiungi il server Airly", + "requests_per_day": "Richieste consentite al giorno", + "requests_remaining": "Richieste consentite rimanenti" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/ko.json new file mode 100644 index 00000000000..1f9db4a592e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/ko.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "wrong_location": "\uc774 \uc9c0\uc5ed\uc5d0\ub294 Airly \uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc774 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "api_key": "API \ud0a4", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "name": "\uc774\ub984" + }, + "description": "Airly \uacf5\uae30 \ud488\uc9c8 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://developer.airly.eu/register \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Airly \uc11c\ubc84 \uc5f0\uacb0", + "requests_per_day": "\uc77c\uc77c \ud5c8\uc6a9 \uc694\uccad \ud69f\uc218", + "requests_remaining": "\ub0a8\uc740 \ud5c8\uc6a9 \uc694\uccad \ud69f\uc218" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/lb.json new file mode 100644 index 00000000000..dd24ee3066f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/lb.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Standuert ass scho konfigur\u00e9iert" + }, + "error": { + "invalid_api_key": "Ong\u00ebltegen API Schl\u00ebssel", + "wrong_location": "Keng Airly Moos Statioun an d\u00ebsem Ber\u00e4ich" + }, + "step": { + "user": { + "data": { + "api_key": "API Schl\u00ebssel", + "latitude": "Breedegrad", + "longitude": "L\u00e4ngegrad", + "name": "Numm" + }, + "description": "Airly Loft Qualit\u00e9it Integratioun ariichten. Fir een API Schl\u00ebssel z'erstelle gitt op https://developer.airly.eu/register", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Airly Server ereechbar" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/nl.json new file mode 100644 index 00000000000..14cbaf1711e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/nl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Locatie is al geconfigureerd." + }, + "error": { + "invalid_api_key": "Ongeldige API-sleutel", + "wrong_location": "Geen Airly meetstations in dit gebied." + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "name": "Naam" + }, + "description": "Airly-integratie van luchtkwaliteit instellen. Ga naar https://developer.airly.eu/register om de API-sleutel te genereren", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Kan Airly server bereiken", + "requests_per_day": "Toegestane verzoeken per dag", + "requests_remaining": "Resterende toegestane verzoeken" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/nn.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/nn.json new file mode 100644 index 00000000000..9cf2b5d70fb --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/nn.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Airly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/no.json new file mode 100644 index 00000000000..4c81422d93c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/no.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Plasseringen er allerede konfigurert" + }, + "error": { + "invalid_api_key": "Ugyldig API-n\u00f8kkel", + "wrong_location": "Ingen Airly m\u00e5lestasjoner i dette omr\u00e5det." + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "name": "Navn" + }, + "description": "Sett opp Airly luftkvalitet integrasjon. For \u00e5 opprette API-n\u00f8kkel, g\u00e5 til [https://developer.airly.eu/register](https://developer.airly.eu/register)", + "title": "" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "N\u00e5 Airly-serveren", + "requests_per_day": "Tillatte foresp\u00f8rsler per dag", + "requests_remaining": "Gjenv\u00e6rende tillatte foresp\u00f8rsler" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/pl.json new file mode 100644 index 00000000000..f205a569474 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/pl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Lokalizacja jest ju\u017c skonfigurowana" + }, + "error": { + "invalid_api_key": "Nieprawid\u0142owy klucz API", + "wrong_location": "Brak stacji pomiarowych Airly w tym rejonie" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa" + }, + "description": "Konfiguracja integracji Airly. By wygenerowa\u0107 klucz API, przejd\u017a na stron\u0119 https://developer.airly.eu/register", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Dost\u0119p do serwera Airly", + "requests_per_day": "Dozwolone dzienne \u017c\u0105dania", + "requests_remaining": "Pozosta\u0142o dozwolonych \u017c\u0105da\u0144" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/pt.json new file mode 100644 index 00000000000..6ebb22b565a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, + "error": { + "invalid_api_key": "Chave de API inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "api_key": "", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nome" + }, + "title": "" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/ru.json new file mode 100644 index 00000000000..41ca90a8c02 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/ru.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "wrong_location": "\u0412 \u044d\u0442\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0438 \u043d\u0435\u0442 \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0441\u0442\u0430\u043d\u0446\u0438\u0439 Airly." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u043f\u043e \u0430\u043d\u0430\u043b\u0438\u0437\u0443 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u0432\u043e\u0437\u0434\u0443\u0445\u0430 Airly. \u0427\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://developer.airly.eu/register.", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 Airly", + "requests_per_day": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0432 \u0434\u0435\u043d\u044c", + "requests_remaining": "\u0421\u0447\u0451\u0442\u0447\u0438\u043a \u043e\u0441\u0442\u0430\u0432\u0448\u0438\u0445\u0441\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/sl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/sl.json new file mode 100644 index 00000000000..e1c89501394 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/sl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Airly integracija za te koordinate je \u017ee nastavljen." + }, + "error": { + "wrong_location": "Na tem obmo\u010dju ni merilnih postaj Airly." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API klju\u010d", + "latitude": "Zemljepisna \u0161irina", + "longitude": "Zemljepisna dol\u017eina", + "name": "Ime integracije" + }, + "description": "Nastavite Airly integracijo za kakovost zraka. \u010ce \u017eelite ustvariti API klju\u010d pojdite na https://developer.airly.eu/register", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Dostop do Airly stre\u017enika" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/sv.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/sv.json new file mode 100644 index 00000000000..e47cb5cd328 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Airly-integrationen f\u00f6r dessa koordinater \u00e4r redan konfigurerad." + }, + "error": { + "wrong_location": "Inga Airly m\u00e4tstationer i detta omr\u00e5de." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API-nyckel", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Integrationens namn" + }, + "description": "Konfigurera integration av luftkvalitet. F\u00f6r att skapa API-nyckel, g\u00e5 till https://developer.airly.eu/register", + "title": "Airly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/tr.json new file mode 100644 index 00000000000..144acc1e1ae --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/tr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Airly sunucusuna eri\u015fin" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/uk.json new file mode 100644 index 00000000000..51bcf5195df --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/uk.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "error": { + "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API", + "wrong_location": "\u0423 \u0446\u0456\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0456 \u043d\u0435\u043c\u0430\u0454 \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043b\u044c\u043d\u0438\u0445 \u0441\u0442\u0430\u043d\u0446\u0456\u0439 Airly." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0441\u0435\u0440\u0432\u0456\u0441\u0443 \u0437 \u0430\u043d\u0430\u043b\u0456\u0437\u0443 \u044f\u043a\u043e\u0441\u0442\u0456 \u043f\u043e\u0432\u0456\u0442\u0440\u044f Airly. \u0429\u043e\u0431 \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0437\u0430 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c https://developer.airly.eu/register.", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Airly" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/zh-Hans.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/zh-Hans.json new file mode 100644 index 00000000000..0f3c5137d31 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/zh-Hans.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u7801", + "latitude": "\u7eac\u5ea6", + "longitude": "\u7ecf\u5ea6", + "name": "\u540d\u79f0" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u53ef\u8bbf\u95ee Airly \u670d\u52a1\u5668", + "requests_per_day": "\u5141\u8bb8\u6bcf\u5929\u8bf7\u6c42" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/zh-Hant.json new file mode 100644 index 00000000000..19ef2ae7532 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airly/translations/zh-Hant.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "invalid_api_key": "API \u5bc6\u9470\u7121\u6548", + "wrong_location": "\u8a72\u5340\u57df\u6c92\u6709 Arily \u76e3\u6e2c\u7ad9\u3002" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u540d\u7a31" + }, + "description": "\u6b32\u8a2d\u5b9a Airly \u7a7a\u6c23\u54c1\u8cea\u6574\u5408\u3002\u8acb\u81f3 https://developer.airly.eu/register \u7522\u751f API \u5bc6\u9470", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u9023\u7dda Airly \u4f3a\u670d\u5668", + "requests_per_day": "\u6bcf\u65e5\u5141\u8a31\u7684\u8acb\u6c42", + "requests_remaining": "\u5176\u9918\u5141\u8a31\u7684\u8acb\u6c42" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/__init__.py new file mode 100644 index 00000000000..0b27a4a9dfd --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/__init__.py @@ -0,0 +1,143 @@ +"""The AirNow integration.""" +import datetime +import logging + +from aiohttp.client_exceptions import ClientConnectorError +from pyairnow import WebServiceAPI +from pyairnow.conv import aqi_to_concentration +from pyairnow.errors import AirNowError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + ATTR_API_AQI, + ATTR_API_AQI_DESCRIPTION, + ATTR_API_AQI_LEVEL, + ATTR_API_AQI_PARAM, + ATTR_API_CAT_DESCRIPTION, + ATTR_API_CAT_LEVEL, + ATTR_API_CATEGORY, + ATTR_API_PM25, + ATTR_API_POLLUTANT, + ATTR_API_REPORT_DATE, + ATTR_API_REPORT_HOUR, + ATTR_API_STATE, + ATTR_API_STATION, + ATTR_API_STATION_LATITUDE, + ATTR_API_STATION_LONGITUDE, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) +PLATFORMS = ["sensor"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up AirNow from a config entry.""" + api_key = entry.data[CONF_API_KEY] + latitude = entry.data[CONF_LATITUDE] + longitude = entry.data[CONF_LONGITUDE] + distance = entry.data[CONF_RADIUS] + + # Reports are published hourly but update twice per hour + update_interval = datetime.timedelta(minutes=30) + + # Setup the Coordinator + session = async_get_clientsession(hass) + coordinator = AirNowDataUpdateCoordinator( + hass, session, api_key, latitude, longitude, distance, update_interval + ) + + # Sync with Coordinator + await coordinator.async_config_entry_first_refresh() + + # Store Entity and Initialize Platforms + hass.data.setdefault(DOMAIN, {}) + 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): + """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 AirNowDataUpdateCoordinator(DataUpdateCoordinator): + """Define an object to hold Airly data.""" + + def __init__( + self, hass, session, api_key, latitude, longitude, distance, update_interval + ): + """Initialize.""" + self.latitude = latitude + self.longitude = longitude + self.distance = distance + + self.airnow = WebServiceAPI(api_key, session=session) + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + + async def _async_update_data(self): + """Update data via library.""" + data = {} + try: + obs = await self.airnow.observations.latLong( + self.latitude, + self.longitude, + distance=self.distance, + ) + + except (AirNowError, ClientConnectorError) as error: + raise UpdateFailed(error) from error + + if not obs: + raise UpdateFailed("No data was returned from AirNow") + + max_aqi = 0 + max_aqi_level = 0 + max_aqi_desc = "" + max_aqi_poll = "" + for obv in obs: + # Convert AQIs to Concentration + pollutant = obv[ATTR_API_AQI_PARAM] + concentration = aqi_to_concentration(obv[ATTR_API_AQI], pollutant) + data[obv[ATTR_API_AQI_PARAM]] = concentration + + # Overall AQI is the max of all pollutant AQIs + if obv[ATTR_API_AQI] > max_aqi: + max_aqi = obv[ATTR_API_AQI] + max_aqi_level = obv[ATTR_API_CATEGORY][ATTR_API_CAT_LEVEL] + max_aqi_desc = obv[ATTR_API_CATEGORY][ATTR_API_CAT_DESCRIPTION] + max_aqi_poll = pollutant + + # Copy other data from PM2.5 Value + if obv[ATTR_API_AQI_PARAM] == ATTR_API_PM25: + # Copy Report Details + data[ATTR_API_REPORT_DATE] = obv[ATTR_API_REPORT_DATE] + data[ATTR_API_REPORT_HOUR] = obv[ATTR_API_REPORT_HOUR] + + # Copy Station Details + data[ATTR_API_STATE] = obv[ATTR_API_STATE] + data[ATTR_API_STATION] = obv[ATTR_API_STATION] + data[ATTR_API_STATION_LATITUDE] = obv[ATTR_API_STATION_LATITUDE] + data[ATTR_API_STATION_LONGITUDE] = obv[ATTR_API_STATION_LONGITUDE] + + # Store Overall AQI + data[ATTR_API_AQI] = max_aqi + data[ATTR_API_AQI_LEVEL] = max_aqi_level + data[ATTR_API_AQI_DESCRIPTION] = max_aqi_desc + data[ATTR_API_POLLUTANT] = max_aqi_poll + + return data diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/config_flow.py new file mode 100644 index 00000000000..d4e39c63c27 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/config_flow.py @@ -0,0 +1,109 @@ +"""Config flow for AirNow integration.""" +import logging + +from pyairnow import WebServiceAPI +from pyairnow.errors import AirNowError, InvalidKeyError +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def validate_input(hass: core.HomeAssistant, data): + """ + Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + session = async_get_clientsession(hass) + client = WebServiceAPI(data[CONF_API_KEY], session=session) + + lat = data[CONF_LATITUDE] + lng = data[CONF_LONGITUDE] + distance = data[CONF_RADIUS] + + # Check that the provided latitude/longitude provide a response + try: + test_data = await client.observations.latLong(lat, lng, distance=distance) + + except InvalidKeyError as exc: + raise InvalidAuth from exc + except AirNowError as exc: + raise CannotConnect from exc + + if not test_data: + raise InvalidLocation + + # Validation Succeeded + return True + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for AirNow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + # Set a unique id based on latitude/longitude + await self.async_set_unique_id( + f"{user_input[CONF_LATITUDE]}-{user_input[CONF_LONGITUDE]}" + ) + self._abort_if_unique_id_configured() + + try: + # Validate inputs + await validate_input(self.hass, user_input) + + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except InvalidLocation: + errors["base"] = "invalid_location" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + # Create Entry + return self.async_create_entry( + title=f"AirNow Sensor at {user_input[CONF_LATITUDE]}, {user_input[CONF_LONGITUDE]}", + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_API_KEY): str, + vol.Optional( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Optional( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + vol.Optional(CONF_RADIUS, default=150): int, + } + ), + errors=errors, + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" + + +class InvalidLocation(exceptions.HomeAssistantError): + """Error to indicate the location is invalid.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/const.py new file mode 100644 index 00000000000..67a9289efc5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/const.py @@ -0,0 +1,21 @@ +"""Constants for the AirNow integration.""" +ATTR_API_AQI = "AQI" +ATTR_API_AQI_LEVEL = "Category.Number" +ATTR_API_AQI_DESCRIPTION = "Category.Name" +ATTR_API_AQI_PARAM = "ParameterName" +ATTR_API_CATEGORY = "Category" +ATTR_API_CAT_LEVEL = "Number" +ATTR_API_CAT_DESCRIPTION = "Name" +ATTR_API_O3 = "O3" +ATTR_API_PM25 = "PM2.5" +ATTR_API_POLLUTANT = "Pollutant" +ATTR_API_REPORT_DATE = "HourObserved" +ATTR_API_REPORT_HOUR = "DateObserved" +ATTR_API_STATE = "StateCode" +ATTR_API_STATION = "ReportingArea" +ATTR_API_STATION_LATITUDE = "Latitude" +ATTR_API_STATION_LONGITUDE = "Longitude" +DEFAULT_NAME = "AirNow" +DOMAIN = "airnow" +SENSOR_AQI_ATTR_DESCR = "description" +SENSOR_AQI_ATTR_LEVEL = "level" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/manifest.json new file mode 100644 index 00000000000..d4e7bc71937 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "airnow", + "name": "AirNow", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/airnow", + "requirements": ["pyairnow==1.1.0"], + "codeowners": ["@asymworks"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/sensor.py new file mode 100644 index 00000000000..2d3adc8d1e2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/sensor.py @@ -0,0 +1,119 @@ +"""Support for the AirNow sensor service.""" +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_DEVICE_CLASS, + ATTR_ICON, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, +) +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ( + ATTR_API_AQI, + ATTR_API_AQI_DESCRIPTION, + ATTR_API_AQI_LEVEL, + ATTR_API_O3, + ATTR_API_PM25, + DOMAIN, + SENSOR_AQI_ATTR_DESCR, + SENSOR_AQI_ATTR_LEVEL, +) + +ATTRIBUTION = "Data provided by AirNow" + +ATTR_LABEL = "label" +ATTR_UNIT = "unit" + +PARALLEL_UPDATES = 1 + +SENSOR_TYPES = { + ATTR_API_AQI: { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_LABEL: ATTR_API_AQI, + ATTR_UNIT: "aqi", + }, + ATTR_API_PM25: { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_LABEL: ATTR_API_PM25, + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + }, + ATTR_API_O3: { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_LABEL: ATTR_API_O3, + ATTR_UNIT: CONCENTRATION_PARTS_PER_MILLION, + }, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up AirNow sensor entities based on a config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + + sensors = [] + for sensor in SENSOR_TYPES: + sensors.append(AirNowSensor(coordinator, sensor)) + + async_add_entities(sensors, False) + + +class AirNowSensor(CoordinatorEntity, SensorEntity): + """Define an AirNow sensor.""" + + def __init__(self, coordinator, kind): + """Initialize.""" + super().__init__(coordinator) + self.kind = kind + self._device_class = None + self._state = None + self._icon = None + self._unit_of_measurement = None + self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def name(self): + """Return the name.""" + return f"AirNow {SENSOR_TYPES[self.kind][ATTR_LABEL]}" + + @property + def state(self): + """Return the state.""" + self._state = self.coordinator.data[self.kind] + return self._state + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + if self.kind == ATTR_API_AQI: + self._attrs[SENSOR_AQI_ATTR_DESCR] = self.coordinator.data[ + ATTR_API_AQI_DESCRIPTION + ] + self._attrs[SENSOR_AQI_ATTR_LEVEL] = self.coordinator.data[ + ATTR_API_AQI_LEVEL + ] + + return self._attrs + + @property + def icon(self): + """Return the icon.""" + self._icon = SENSOR_TYPES[self.kind][ATTR_ICON] + return self._icon + + @property + def device_class(self): + """Return the device_class.""" + return SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return f"{self.coordinator.latitude}-{self.coordinator.longitude}-{self.kind.lower()}" + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return SENSOR_TYPES[self.kind][ATTR_UNIT] diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/strings.json new file mode 100644 index 00000000000..a73ad6d179c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/strings.json @@ -0,0 +1,26 @@ +{ + "title": "AirNow", + "config": { + "step": { + "user": { + "title": "AirNow", + "description": "Set up AirNow air quality integration. To generate API key go to https://docs.airnowapi.org/account/request/", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]", + "radius": "Station Radius (miles; optional)" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_location": "No results found for that location", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/bg.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/bg.json new file mode 100644 index 00000000000..5d274ec2b73 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/ca.json new file mode 100644 index 00000000000..2db3cfad563 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/ca.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_location": "No s'ha trobat cap resultat per a aquesta ubicaci\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "latitude": "Latitud", + "longitude": "Longitud", + "radius": "Radi de l'estaci\u00f3 (milles; opcional)" + }, + "description": "Configura la integraci\u00f3 de qualitat d'aire AirNow. Per generar la clau API, v\u00e9s a https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/cs.json new file mode 100644 index 00000000000..d978e44c70a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/cs.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka" + }, + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/de.json new file mode 100644 index 00000000000..8c2b47c1bd4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/de.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_location": "F\u00fcr diesen Standort wurden keine Ergebnisse gefunden", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad" + }, + "description": "Richten Sie die AirNow-Luftqualit\u00e4tsintegration ein. Um den API-Schl\u00fcssel zu generieren, besuchen Sie https://docs.airnowapi.org/account/request/.", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/en.json new file mode 100644 index 00000000000..371bb270ac1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_location": "No results found for that location", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude", + "radius": "Station Radius (miles; optional)" + }, + "description": "Set up AirNow air quality integration. To generate API key go to https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/es-419.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/es-419.json new file mode 100644 index 00000000000..015d7242ef1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_location": "No se encontraron resultados para esa ubicaci\u00f3n" + }, + "step": { + "user": { + "data": { + "radius": "Radio de la estaci\u00f3n (millas; opcional)" + }, + "description": "Configure la integraci\u00f3n de la calidad del aire de AirNow. Para generar la clave de API, vaya a https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/es.json new file mode 100644 index 00000000000..d6a228a6e27 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_location": "No se han encontrado resultados para esa ubicaci\u00f3n", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Clave API", + "latitude": "Latitud", + "longitude": "Longitud", + "radius": "Radio de la estaci\u00f3n (millas; opcional)" + }, + "description": "Configurar la integraci\u00f3n de calidad del aire de AirNow. Para generar una clave API, ve a https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/et.json new file mode 100644 index 00000000000..52b2bb618e0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/et.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine", + "invalid_location": "Selle asukoha jaoks ei leitud andmeid", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad", + "radius": "Jaama raadius (miilid; valikuline)" + }, + "description": "Seadista AirNow \u00f5hukvaliteedi sidumine. API-v\u00f5tme loomiseks mine aadressile https://docs.airnowapi.org/account/request/", + "title": "" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/fr.json new file mode 100644 index 00000000000..ff85d9318e9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/fr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec \u00e0 la connexion", + "invalid_auth": "Authentification invalide", + "invalid_location": "Aucun r\u00e9sultat trouv\u00e9 pour cet emplacement", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 API", + "latitude": "Latitude", + "longitude": "Longitude", + "radius": "Rayon d'action de la station (en miles, facultatif)" + }, + "description": "Configurez l'int\u00e9gration de la qualit\u00e9 de l'air AirNow. Pour g\u00e9n\u00e9rer la cl\u00e9 API, acc\u00e9dez \u00e0 https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/hu.json new file mode 100644 index 00000000000..418450f2419 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/hu.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_location": "Erre a helyre nem tal\u00e1lhat\u00f3 eredm\u00e9ny", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g" + }, + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/id.json new file mode 100644 index 00000000000..66fdff72fae --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/id.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "invalid_location": "Tidak ada hasil yang ditemukan untuk lokasi tersebut", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "latitude": "Lintang", + "longitude": "Bujur", + "radius": "Radius Stasiun (mil; opsional)" + }, + "description": "Siapkan integrasi kualitas udara AirNow. Untuk membuat kunci API buka https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/it.json new file mode 100644 index 00000000000..9dda15dfbd2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "invalid_location": "Nessun risultato trovato per quella localit\u00e0", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "latitude": "Latitudine", + "longitude": "Logitudine", + "radius": "Raggio stazione (miglia; opzionale)" + }, + "description": "Configura l'integrazione per la qualit\u00e0 dell'aria AirNow. Per generare la chiave API, vai su https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/ko.json new file mode 100644 index 00000000000..adfbf0be8ed --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/ko.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_location": "\ud574\ub2f9 \uc704\uce58\uc5d0 \uacb0\uacfc\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "api_key": "API \ud0a4", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "radius": "\uce21\uc815 \uc2a4\ud14c\uc774\uc158 \ubc18\uacbd (\ub9c8\uc77c; \uc120\ud0dd \uc0ac\ud56d)" + }, + "description": "AirNow \uacf5\uae30 \ud488\uc9c8 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://docs.airnowapi.org/account/request \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/lb.json new file mode 100644 index 00000000000..a62bd0bf478 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/lb.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "invalid_location": "Keng Resultater fonnt fir d\u00ebse Standuert", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "api_key": "API Schl\u00ebssel", + "latitude": "L\u00e4ngegrad", + "longitude": "Breedegrag" + }, + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/nl.json new file mode 100644 index 00000000000..090a5363823 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/nl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "invalid_location": "Geen resultaten gevonden voor die locatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "radius": "Stationsradius (mijl; optioneel)" + }, + "description": "AirNow luchtkwaliteit integratie opzetten. Om een API sleutel te genereren ga naar https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/no.json new file mode 100644 index 00000000000..19fa7e12207 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "invalid_location": "Ingen resultater funnet for den plasseringen", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "radius": "Stasjonsradius (miles; valgfritt)" + }, + "description": "Konfigurer integrering av luftkvalitet i AirNow. For \u00e5 generere en API-n\u00f8kkel, g\u00e5r du til https://docs.airnowapi.org/account/request/", + "title": "" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/pl.json new file mode 100644 index 00000000000..fe4310607b9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/pl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_location": "Brak wynik\u00f3w dla tej lokalizacji", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "radius": "Promie\u0144 od stacji (w milach; opcjonalnie)" + }, + "description": "Konfiguracja integracji jako\u015bci powietrza AirNow. Aby wygenerowa\u0107 klucz API, przejd\u017a do https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/pt.json new file mode 100644 index 00000000000..3aa509dd6e8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/pt.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude" + }, + "title": "" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/ru.json new file mode 100644 index 00000000000..9667accb7c4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/ru.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_location": "\u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u043e\u0432 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e.", + "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", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 (\u0432 \u043c\u0438\u043b\u044f\u0445; \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" + }, + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u043f\u043e \u0430\u043d\u0430\u043b\u0438\u0437\u0443 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u0432\u043e\u0437\u0434\u0443\u0445\u0430 AirNow. \u0427\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://docs.airnowapi.org/account/request/.", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/tr.json new file mode 100644 index 00000000000..06af714dc87 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_location": "Bu konum i\u00e7in hi\u00e7bir sonu\u00e7 bulunamad\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam", + "radius": "\u0130stasyon Yar\u0131\u00e7ap\u0131 (mil; iste\u011fe ba\u011fl\u0131)" + }, + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/uk.json new file mode 100644 index 00000000000..bb872123f54 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/uk.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "invalid_location": "\u041d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0456\u0432 \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", + "radius": "\u0420\u0430\u0434\u0456\u0443\u0441 \u0441\u0442\u0430\u043d\u0446\u0456\u0457 (\u043c\u0438\u043b\u0456; \u043d\u0435\u043e\u0431\u043e\u0432\u2019\u044f\u0437\u043a\u043e\u0432\u043e)" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e \u044f\u043a\u043e\u0441\u0442\u0456 \u043f\u043e\u0432\u0456\u0442\u0440\u044f AirNow. \u0429\u043e\u0431 \u0437\u0433\u0435\u043d\u0435\u0440\u0443\u0432\u0430\u0442\u0438 \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043d\u0430 \u0441\u0442\u043e\u0440\u0456\u043d\u043a\u0443 https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/zh-Hant.json new file mode 100644 index 00000000000..0cdb4a11bed --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airnow/translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_location": "\u627e\u4e0d\u5230\u8a72\u4f4d\u7f6e\u7684\u7d50\u679c", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "radius": "\u89c0\u6e2c\u7ad9\u534a\u5f91\uff08\u82f1\u91cc\uff1b\u9078\u9805\uff09" + }, + "description": "\u6b32\u8a2d\u5b9a AirNow \u7a7a\u6c23\u54c1\u8cea\u6574\u5408\u3002\u8acb\u81f3 https://docs.airnowapi.org/account/request/ \u7522\u751f API \u5bc6\u9470", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/__init__.py new file mode 100644 index 00000000000..ac34c16d3d0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/__init__.py @@ -0,0 +1,374 @@ +"""The airvisual component.""" +from datetime import timedelta +from math import ceil + +from pyairvisual import CloudAPI, NodeSamba +from pyairvisual.errors import ( + AirVisualError, + InvalidKeyError, + KeyExpiredError, + NodeProError, +) + +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONF_API_KEY, + CONF_IP_ADDRESS, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_PASSWORD, + CONF_SHOW_ON_MAP, + CONF_STATE, +) +from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import ( + CONF_CITY, + CONF_COUNTRY, + CONF_GEOGRAPHIES, + CONF_INTEGRATION_TYPE, + DATA_COORDINATOR, + DOMAIN, + INTEGRATION_TYPE_GEOGRAPHY_COORDS, + INTEGRATION_TYPE_GEOGRAPHY_NAME, + INTEGRATION_TYPE_NODE_PRO, + LOGGER, +) + +PLATFORMS = ["air_quality", "sensor"] + +DATA_LISTENER = "listener" + +DEFAULT_ATTRIBUTION = "Data provided by AirVisual" +DEFAULT_NODE_PRO_UPDATE_INTERVAL = timedelta(minutes=1) + +CONFIG_SCHEMA = cv.deprecated(DOMAIN) + + +@callback +def async_get_geography_id(geography_dict): + """Generate a unique ID from a geography dict.""" + if not geography_dict: + return + + if CONF_CITY in geography_dict: + return ", ".join( + ( + geography_dict[CONF_CITY], + geography_dict[CONF_STATE], + geography_dict[CONF_COUNTRY], + ) + ) + return ", ".join( + (str(geography_dict[CONF_LATITUDE]), str(geography_dict[CONF_LONGITUDE])) + ) + + +@callback +def async_get_cloud_api_update_interval(hass, api_key, num_consumers): + """Get a leveled scan interval for a particular cloud API key. + + This will shift based on the number of active consumers, thus keeping the user + under the monthly API limit. + """ + # Assuming 10,000 calls per month and a "largest possible month" of 31 days; note + # that we give a buffer of 1500 API calls for any drift, restarts, etc.: + minutes_between_api_calls = ceil(num_consumers * 31 * 24 * 60 / 8500) + + LOGGER.debug( + "Leveling API key usage (%s): %s consumers, %s minutes between updates", + api_key, + num_consumers, + minutes_between_api_calls, + ) + + return timedelta(minutes=minutes_between_api_calls) + + +@callback +def async_get_cloud_coordinators_by_api_key(hass, api_key): + """Get all DataUpdateCoordinator objects related to a particular API key.""" + coordinators = [] + for entry_id, coordinator in hass.data[DOMAIN][DATA_COORDINATOR].items(): + config_entry = hass.config_entries.async_get_entry(entry_id) + if config_entry.data.get(CONF_API_KEY) == api_key: + coordinators.append(coordinator) + return coordinators + + +@callback +def async_sync_geo_coordinator_update_intervals(hass, api_key): + """Sync the update interval for geography-based data coordinators (by API key).""" + coordinators = async_get_cloud_coordinators_by_api_key(hass, api_key) + + if not coordinators: + return + + update_interval = async_get_cloud_api_update_interval( + hass, api_key, len(coordinators) + ) + + for coordinator in coordinators: + LOGGER.debug( + "Updating interval for coordinator: %s, %s", + coordinator.name, + update_interval, + ) + coordinator.update_interval = update_interval + + +async def async_setup(hass, config): + """Set up the AirVisual component.""" + hass.data[DOMAIN] = {DATA_COORDINATOR: {}, DATA_LISTENER: {}} + return True + + +@callback +def _standardize_geography_config_entry(hass, config_entry): + """Ensure that geography config entries have appropriate properties.""" + entry_updates = {} + + if not config_entry.unique_id: + # If the config entry doesn't already have a unique ID, set one: + entry_updates["unique_id"] = config_entry.data[CONF_API_KEY] + if not config_entry.options: + # If the config entry doesn't already have any options set, set defaults: + entry_updates["options"] = {CONF_SHOW_ON_MAP: True} + if config_entry.data.get(CONF_INTEGRATION_TYPE) not in [ + INTEGRATION_TYPE_GEOGRAPHY_COORDS, + INTEGRATION_TYPE_GEOGRAPHY_NAME, + ]: + # If the config entry data doesn't contain an integration type that we know + # about, infer it from the data we have: + entry_updates["data"] = {**config_entry.data} + if CONF_CITY in config_entry.data: + entry_updates["data"][ + CONF_INTEGRATION_TYPE + ] = INTEGRATION_TYPE_GEOGRAPHY_NAME + else: + entry_updates["data"][ + CONF_INTEGRATION_TYPE + ] = INTEGRATION_TYPE_GEOGRAPHY_COORDS + + if not entry_updates: + return + + hass.config_entries.async_update_entry(config_entry, **entry_updates) + + +@callback +def _standardize_node_pro_config_entry(hass, config_entry): + """Ensure that Node/Pro config entries have appropriate properties.""" + entry_updates = {} + + if CONF_INTEGRATION_TYPE not in config_entry.data: + # If the config entry data doesn't contain the integration type, add it: + entry_updates["data"] = { + **config_entry.data, + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO, + } + + if not entry_updates: + return + + hass.config_entries.async_update_entry(config_entry, **entry_updates) + + +async def async_setup_entry(hass, config_entry): + """Set up AirVisual as config entry.""" + if CONF_API_KEY in config_entry.data: + _standardize_geography_config_entry(hass, config_entry) + + websession = aiohttp_client.async_get_clientsession(hass) + cloud_api = CloudAPI(config_entry.data[CONF_API_KEY], session=websession) + + async def async_update_data(): + """Get new data from the API.""" + if CONF_CITY in config_entry.data: + api_coro = cloud_api.air_quality.city( + config_entry.data[CONF_CITY], + config_entry.data[CONF_STATE], + config_entry.data[CONF_COUNTRY], + ) + else: + api_coro = cloud_api.air_quality.nearest_city( + config_entry.data[CONF_LATITUDE], + config_entry.data[CONF_LONGITUDE], + ) + + try: + return await api_coro + except (InvalidKeyError, KeyExpiredError) as ex: + raise ConfigEntryAuthFailed from ex + except AirVisualError as err: + raise UpdateFailed(f"Error while retrieving data: {err}") from err + + coordinator = DataUpdateCoordinator( + hass, + LOGGER, + name=async_get_geography_id(config_entry.data), + # We give a placeholder update interval in order to create the coordinator; + # then, below, we use the coordinator's presence (along with any other + # coordinators using the same API key) to calculate an actual, leveled + # update interval: + update_interval=timedelta(minutes=5), + update_method=async_update_data, + ) + + # Only geography-based entries have options: + hass.data[DOMAIN][DATA_LISTENER][ + config_entry.entry_id + ] = config_entry.add_update_listener(async_reload_entry) + else: + _standardize_node_pro_config_entry(hass, config_entry) + + async def async_update_data(): + """Get new data from the API.""" + try: + async with NodeSamba( + config_entry.data[CONF_IP_ADDRESS], config_entry.data[CONF_PASSWORD] + ) as node: + return await node.async_get_latest_measurements() + except NodeProError as err: + raise UpdateFailed(f"Error while retrieving data: {err}") from err + + coordinator = DataUpdateCoordinator( + hass, + LOGGER, + name="Node/Pro data", + update_interval=DEFAULT_NODE_PRO_UPDATE_INTERVAL, + update_method=async_update_data, + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator + + # Reassess the interval between 2 server requests + if CONF_API_KEY in config_entry.data: + async_sync_geo_coordinator_update_intervals( + hass, config_entry.data[CONF_API_KEY] + ) + + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + + return True + + +async def async_migrate_entry(hass, config_entry): + """Migrate an old config entry.""" + version = config_entry.version + + LOGGER.debug("Migrating from version %s", version) + + # 1 -> 2: One geography per config entry + if version == 1: + version = config_entry.version = 2 + + # Update the config entry to only include the first geography (there is always + # guaranteed to be at least one): + geographies = list(config_entry.data[CONF_GEOGRAPHIES]) + first_geography = geographies.pop(0) + first_id = async_get_geography_id(first_geography) + + hass.config_entries.async_update_entry( + config_entry, + unique_id=first_id, + title=f"Cloud API ({first_id})", + data={CONF_API_KEY: config_entry.data[CONF_API_KEY], **first_geography}, + ) + + # For any geographies that remain, create a new config entry for each one: + for geography in geographies: + if CONF_LATITUDE in geography: + source = "geography_by_coords" + else: + source = "geography_by_name" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": source}, + data={CONF_API_KEY: config_entry.data[CONF_API_KEY], **geography}, + ) + ) + + LOGGER.info("Migration to version %s successful", version) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload an AirVisual config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + + if unload_ok: + hass.data[DOMAIN][DATA_COORDINATOR].pop(config_entry.entry_id) + remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id) + remove_listener() + + if CONF_API_KEY in config_entry.data: + # Re-calculate the update interval period for any remaining consumers of + # this API key: + async_sync_geo_coordinator_update_intervals( + hass, config_entry.data[CONF_API_KEY] + ) + + return unload_ok + + +async def async_reload_entry(hass, config_entry): + """Handle an options update.""" + await hass.config_entries.async_reload(config_entry.entry_id) + + +class AirVisualEntity(CoordinatorEntity): + """Define a generic AirVisual entity.""" + + def __init__(self, coordinator): + """Initialize.""" + super().__init__(coordinator) + self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._icon = None + self._unit = None + + @property + def extra_state_attributes(self): + """Return the device state attributes.""" + return self._attrs + + @property + def icon(self): + """Return the icon.""" + return self._icon + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + async def async_added_to_hass(self): + """Register callbacks.""" + + @callback + def update(): + """Update the state.""" + self.update_from_latest_data() + self.async_write_ha_state() + + self.async_on_remove(self.coordinator.async_add_listener(update)) + + self.update_from_latest_data() + + @callback + def update_from_latest_data(self): + """Update the entity from the latest data.""" + raise NotImplementedError diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/air_quality.py b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/air_quality.py new file mode 100644 index 00000000000..047367fa67c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/air_quality.py @@ -0,0 +1,110 @@ +"""Support for AirVisual Node/Pro units.""" +from homeassistant.components.air_quality import AirQualityEntity +from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER +from homeassistant.core import callback + +from . import AirVisualEntity +from .const import ( + CONF_INTEGRATION_TYPE, + DATA_COORDINATOR, + DOMAIN, + INTEGRATION_TYPE_NODE_PRO, +) + +ATTR_HUMIDITY = "humidity" +ATTR_SENSOR_LIFE = "{0}_sensor_life" +ATTR_VOC = "voc" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up AirVisual air quality entities based on a config entry.""" + # Geography-based AirVisual integrations don't utilize this platform: + if config_entry.data[CONF_INTEGRATION_TYPE] != INTEGRATION_TYPE_NODE_PRO: + return + + coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] + + async_add_entities([AirVisualNodeProSensor(coordinator)], True) + + +class AirVisualNodeProSensor(AirVisualEntity, AirQualityEntity): + """Define a sensor for a AirVisual Node/Pro.""" + + def __init__(self, airvisual): + """Initialize.""" + super().__init__(airvisual) + + self._icon = "mdi:chemical-weapon" + self._unit = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + + @property + def air_quality_index(self): + """Return the Air Quality Index (AQI).""" + if self.coordinator.data["settings"]["is_aqi_usa"]: + return self.coordinator.data["measurements"]["aqi_us"] + return self.coordinator.data["measurements"]["aqi_cn"] + + @property + def available(self): + """Return True if entity is available.""" + return bool(self.coordinator.data) + + @property + def carbon_dioxide(self): + """Return the CO2 (carbon dioxide) level.""" + return self.coordinator.data["measurements"].get("co2") + + @property + def device_info(self): + """Return device registry information for this entity.""" + return { + "identifiers": {(DOMAIN, self.coordinator.data["serial_number"])}, + "name": self.coordinator.data["settings"]["node_name"], + "manufacturer": "AirVisual", + "model": f'{self.coordinator.data["status"]["model"]}', + "sw_version": ( + f'Version {self.coordinator.data["status"]["system_version"]}' + f'{self.coordinator.data["status"]["app_version"]}' + ), + } + + @property + def name(self): + """Return the name.""" + node_name = self.coordinator.data["settings"]["node_name"] + return f"{node_name} Node/Pro: Air Quality" + + @property + def particulate_matter_2_5(self): + """Return the particulate matter 2.5 level.""" + return self.coordinator.data["measurements"].get("pm2_5") + + @property + def particulate_matter_10(self): + """Return the particulate matter 10 level.""" + return self.coordinator.data["measurements"].get("pm1_0") + + @property + def particulate_matter_0_1(self): + """Return the particulate matter 0.1 level.""" + return self.coordinator.data["measurements"].get("pm0_1") + + @property + def unique_id(self): + """Return a unique, Home Assistant friendly identifier for this entity.""" + return self.coordinator.data["serial_number"] + + @callback + def update_from_latest_data(self): + """Update the entity from the latest data.""" + self._attrs.update( + { + ATTR_VOC: self.coordinator.data["measurements"].get("voc"), + **{ + ATTR_SENSOR_LIFE.format(pollutant): lifespan + for pollutant, lifespan in self.coordinator.data["status"][ + "sensor_life" + ].items() + }, + } + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/config_flow.py new file mode 100644 index 00000000000..ef7873a31b1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/config_flow.py @@ -0,0 +1,266 @@ +"""Define a config flow manager for AirVisual.""" +import asyncio + +from pyairvisual import CloudAPI, NodeSamba +from pyairvisual.errors import ( + AirVisualError, + InvalidKeyError, + NodeProError, + NotFoundError, +) +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_API_KEY, + CONF_IP_ADDRESS, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_PASSWORD, + CONF_SHOW_ON_MAP, + CONF_STATE, +) +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client, config_validation as cv + +from . import async_get_geography_id +from .const import ( + CONF_CITY, + CONF_COUNTRY, + CONF_INTEGRATION_TYPE, + DOMAIN, + INTEGRATION_TYPE_GEOGRAPHY_COORDS, + INTEGRATION_TYPE_GEOGRAPHY_NAME, + INTEGRATION_TYPE_NODE_PRO, + LOGGER, +) + +API_KEY_DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): cv.string}) +GEOGRAPHY_NAME_SCHEMA = API_KEY_DATA_SCHEMA.extend( + { + vol.Required(CONF_CITY): cv.string, + vol.Required(CONF_STATE): cv.string, + vol.Required(CONF_COUNTRY): cv.string, + } +) +NODE_PRO_SCHEMA = vol.Schema( + {vol.Required(CONF_IP_ADDRESS): str, vol.Required(CONF_PASSWORD): cv.string} +) +PICK_INTEGRATION_TYPE_SCHEMA = vol.Schema( + { + vol.Required("type"): vol.In( + [ + INTEGRATION_TYPE_GEOGRAPHY_COORDS, + INTEGRATION_TYPE_GEOGRAPHY_NAME, + INTEGRATION_TYPE_NODE_PRO, + ] + ) + } +) + + +class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle an AirVisual config flow.""" + + VERSION = 2 + + def __init__(self): + """Initialize the config flow.""" + self._entry_data_for_reauth = None + self._geo_id = None + + @property + def geography_coords_schema(self): + """Return the data schema for the cloud API.""" + return API_KEY_DATA_SCHEMA.extend( + { + vol.Required( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Required( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + } + ) + + async def _async_finish_geography(self, user_input, integration_type): + """Validate a Cloud API key.""" + websession = aiohttp_client.async_get_clientsession(self.hass) + cloud_api = CloudAPI(user_input[CONF_API_KEY], session=websession) + + # If this is the first (and only the first) time we've seen this API key, check + # that it's valid: + valid_keys = self.hass.data.setdefault("airvisual_checked_api_keys", set()) + valid_keys_lock = self.hass.data.setdefault( + "airvisual_checked_api_keys_lock", asyncio.Lock() + ) + + if integration_type == INTEGRATION_TYPE_GEOGRAPHY_COORDS: + coro = cloud_api.air_quality.nearest_city() + error_schema = self.geography_coords_schema + error_step = "geography_by_coords" + else: + coro = cloud_api.air_quality.city( + user_input[CONF_CITY], user_input[CONF_STATE], user_input[CONF_COUNTRY] + ) + error_schema = GEOGRAPHY_NAME_SCHEMA + error_step = "geography_by_name" + + async with valid_keys_lock: + if user_input[CONF_API_KEY] not in valid_keys: + try: + await coro + except InvalidKeyError: + return self.async_show_form( + step_id=error_step, + data_schema=error_schema, + errors={CONF_API_KEY: "invalid_api_key"}, + ) + except NotFoundError: + return self.async_show_form( + step_id=error_step, + data_schema=error_schema, + errors={CONF_CITY: "location_not_found"}, + ) + except AirVisualError as err: + LOGGER.error(err) + return self.async_show_form( + step_id=error_step, + data_schema=error_schema, + errors={"base": "unknown"}, + ) + + valid_keys.add(user_input[CONF_API_KEY]) + + existing_entry = await self.async_set_unique_id(self._geo_id) + if existing_entry: + self.hass.config_entries.async_update_entry(existing_entry, data=user_input) + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry( + title=f"Cloud API ({self._geo_id})", + data={**user_input, CONF_INTEGRATION_TYPE: integration_type}, + ) + + async def _async_init_geography(self, user_input, integration_type): + """Handle the initialization of the integration via the cloud API.""" + self._geo_id = async_get_geography_id(user_input) + await self._async_set_unique_id(self._geo_id) + self._abort_if_unique_id_configured() + return await self._async_finish_geography(user_input, integration_type) + + async def _async_set_unique_id(self, unique_id): + """Set the unique ID of the config flow and abort if it already exists.""" + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Define the config flow to handle options.""" + return AirVisualOptionsFlowHandler(config_entry) + + async def async_step_geography_by_coords(self, user_input=None): + """Handle the initialization of the cloud API based on latitude/longitude.""" + if not user_input: + return self.async_show_form( + step_id="geography_by_coords", data_schema=self.geography_coords_schema + ) + + return await self._async_init_geography( + user_input, INTEGRATION_TYPE_GEOGRAPHY_COORDS + ) + + async def async_step_geography_by_name(self, user_input=None): + """Handle the initialization of the cloud API based on city/state/country.""" + if not user_input: + return self.async_show_form( + step_id="geography_by_name", data_schema=GEOGRAPHY_NAME_SCHEMA + ) + + return await self._async_init_geography( + user_input, INTEGRATION_TYPE_GEOGRAPHY_NAME + ) + + async def async_step_node_pro(self, user_input=None): + """Handle the initialization of the integration with a Node/Pro.""" + if not user_input: + return self.async_show_form(step_id="node_pro", data_schema=NODE_PRO_SCHEMA) + + await self._async_set_unique_id(user_input[CONF_IP_ADDRESS]) + + node = NodeSamba(user_input[CONF_IP_ADDRESS], user_input[CONF_PASSWORD]) + + try: + await node.async_connect() + except NodeProError as err: + LOGGER.error("Error connecting to Node/Pro unit: %s", err) + return self.async_show_form( + step_id="node_pro", + data_schema=NODE_PRO_SCHEMA, + errors={CONF_IP_ADDRESS: "cannot_connect"}, + ) + + await node.async_disconnect() + + return self.async_create_entry( + title=f"Node/Pro ({user_input[CONF_IP_ADDRESS]})", + data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO}, + ) + + async def async_step_reauth(self, data): + """Handle configuration by re-auth.""" + self._entry_data_for_reauth = data + self._geo_id = async_get_geography_id(data) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Handle re-auth completion.""" + if not user_input: + return self.async_show_form( + step_id="reauth_confirm", data_schema=API_KEY_DATA_SCHEMA + ) + + conf = {CONF_API_KEY: user_input[CONF_API_KEY], **self._entry_data_for_reauth} + + return await self._async_finish_geography( + conf, self._entry_data_for_reauth[CONF_INTEGRATION_TYPE] + ) + + async def async_step_user(self, user_input=None): + """Handle the start of the config flow.""" + if not user_input: + return self.async_show_form( + step_id="user", data_schema=PICK_INTEGRATION_TYPE_SCHEMA + ) + + if user_input["type"] == INTEGRATION_TYPE_GEOGRAPHY_COORDS: + return await self.async_step_geography_by_coords() + if user_input["type"] == INTEGRATION_TYPE_GEOGRAPHY_NAME: + return await self.async_step_geography_by_name() + return await self.async_step_node_pro() + + +class AirVisualOptionsFlowHandler(config_entries.OptionsFlow): + """Handle an AirVisual options flow.""" + + def __init__(self, config_entry): + """Initialize.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required( + CONF_SHOW_ON_MAP, + default=self.config_entry.options.get(CONF_SHOW_ON_MAP), + ): bool + } + ), + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/const.py new file mode 100644 index 00000000000..510ada2b68c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/const.py @@ -0,0 +1,16 @@ +"""Define AirVisual constants.""" +import logging + +DOMAIN = "airvisual" +LOGGER = logging.getLogger(__package__) + +INTEGRATION_TYPE_GEOGRAPHY_COORDS = "Geographical Location by Latitude/Longitude" +INTEGRATION_TYPE_GEOGRAPHY_NAME = "Geographical Location by Name" +INTEGRATION_TYPE_NODE_PRO = "AirVisual Node/Pro" + +CONF_CITY = "city" +CONF_COUNTRY = "country" +CONF_GEOGRAPHIES = "geographies" +CONF_INTEGRATION_TYPE = "integration_type" + +DATA_COORDINATOR = "coordinator" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/manifest.json new file mode 100644 index 00000000000..b94218f6c13 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "airvisual", + "name": "AirVisual", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/airvisual", + "requirements": ["pyairvisual==5.0.8"], + "codeowners": ["@bachya"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/sensor.py new file mode 100644 index 00000000000..1febcec68f4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/sensor.py @@ -0,0 +1,296 @@ +"""Support for AirVisual air quality sensors.""" +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import ( + ATTR_LATITUDE, + ATTR_LONGITUDE, + ATTR_STATE, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_BILLION, + CONCENTRATION_PARTS_PER_MILLION, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_SHOW_ON_MAP, + CONF_STATE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, + TEMP_CELSIUS, +) +from homeassistant.core import callback + +from . import AirVisualEntity +from .const import ( + CONF_CITY, + CONF_COUNTRY, + CONF_INTEGRATION_TYPE, + DATA_COORDINATOR, + DOMAIN, + INTEGRATION_TYPE_GEOGRAPHY_COORDS, + INTEGRATION_TYPE_GEOGRAPHY_NAME, +) + +ATTR_CITY = "city" +ATTR_COUNTRY = "country" +ATTR_POLLUTANT_SYMBOL = "pollutant_symbol" +ATTR_POLLUTANT_UNIT = "pollutant_unit" +ATTR_REGION = "region" + +SENSOR_KIND_LEVEL = "air_pollution_level" +SENSOR_KIND_AQI = "air_quality_index" +SENSOR_KIND_POLLUTANT = "main_pollutant" +SENSOR_KIND_BATTERY_LEVEL = "battery_level" +SENSOR_KIND_HUMIDITY = "humidity" +SENSOR_KIND_TEMPERATURE = "temperature" + +GEOGRAPHY_SENSORS = [ + (SENSOR_KIND_LEVEL, "Air Pollution Level", "mdi:gauge", None), + (SENSOR_KIND_AQI, "Air Quality Index", "mdi:chart-line", "AQI"), + (SENSOR_KIND_POLLUTANT, "Main Pollutant", "mdi:chemical-weapon", None), +] +GEOGRAPHY_SENSOR_LOCALES = {"cn": "Chinese", "us": "U.S."} + +NODE_PRO_SENSORS = [ + (SENSOR_KIND_BATTERY_LEVEL, "Battery", DEVICE_CLASS_BATTERY, PERCENTAGE), + (SENSOR_KIND_HUMIDITY, "Humidity", DEVICE_CLASS_HUMIDITY, PERCENTAGE), + (SENSOR_KIND_TEMPERATURE, "Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS), +] + + +@callback +def async_get_pollutant_label(symbol): + """Get a pollutant's label based on its symbol.""" + if symbol == "co": + return "Carbon Monoxide" + if symbol == "n2": + return "Nitrogen Dioxide" + if symbol == "o3": + return "Ozone" + if symbol == "p1": + return "PM10" + if symbol == "p2": + return "PM2.5" + if symbol == "s2": + return "Sulfur Dioxide" + return symbol + + +@callback +def async_get_pollutant_level_info(value): + """Return a verbal pollutant level (and associated icon) for a numeric value.""" + if 0 <= value <= 50: + return ("Good", "mdi:emoticon-excited") + if 51 <= value <= 100: + return ("Moderate", "mdi:emoticon-happy") + if 101 <= value <= 150: + return ("Unhealthy for sensitive groups", "mdi:emoticon-neutral") + if 151 <= value <= 200: + return ("Unhealthy", "mdi:emoticon-sad") + if 201 <= value <= 300: + return ("Very Unhealthy", "mdi:emoticon-dead") + return ("Hazardous", "mdi:biohazard") + + +@callback +def async_get_pollutant_unit(symbol): + """Get a pollutant's unit based on its symbol.""" + if symbol == "co": + return CONCENTRATION_PARTS_PER_MILLION + if symbol == "n2": + return CONCENTRATION_PARTS_PER_BILLION + if symbol == "o3": + return CONCENTRATION_PARTS_PER_BILLION + if symbol == "p1": + return CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + if symbol == "p2": + return CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + if symbol == "s2": + return CONCENTRATION_PARTS_PER_BILLION + return None + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up AirVisual sensors based on a config entry.""" + coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] + + if config_entry.data[CONF_INTEGRATION_TYPE] in [ + INTEGRATION_TYPE_GEOGRAPHY_COORDS, + INTEGRATION_TYPE_GEOGRAPHY_NAME, + ]: + sensors = [ + AirVisualGeographySensor( + coordinator, + config_entry, + kind, + name, + icon, + unit, + locale, + ) + for locale in GEOGRAPHY_SENSOR_LOCALES + for kind, name, icon, unit in GEOGRAPHY_SENSORS + ] + else: + sensors = [ + AirVisualNodeProSensor(coordinator, kind, name, device_class, unit) + for kind, name, device_class, unit in NODE_PRO_SENSORS + ] + + async_add_entities(sensors, True) + + +class AirVisualGeographySensor(AirVisualEntity, SensorEntity): + """Define an AirVisual sensor related to geography data via the Cloud API.""" + + def __init__(self, coordinator, config_entry, kind, name, icon, unit, locale): + """Initialize.""" + super().__init__(coordinator) + + self._attrs.update( + { + ATTR_CITY: config_entry.data.get(CONF_CITY), + ATTR_STATE: config_entry.data.get(CONF_STATE), + ATTR_COUNTRY: config_entry.data.get(CONF_COUNTRY), + } + ) + self._config_entry = config_entry + self._icon = icon + self._kind = kind + self._locale = locale + self._name = name + self._state = None + self._unit = unit + + @property + def available(self): + """Return True if entity is available.""" + try: + return self.coordinator.last_update_success and bool( + self.coordinator.data["current"]["pollution"] + ) + except KeyError: + return False + + @property + def name(self): + """Return the name.""" + return f"{GEOGRAPHY_SENSOR_LOCALES[self._locale]} {self._name}" + + @property + def state(self): + """Return the state.""" + return self._state + + @property + def unique_id(self): + """Return a unique, Home Assistant friendly identifier for this entity.""" + return f"{self._config_entry.unique_id}_{self._locale}_{self._kind}" + + @callback + def update_from_latest_data(self): + """Update the entity from the latest data.""" + try: + data = self.coordinator.data["current"]["pollution"] + except KeyError: + return + + if self._kind == SENSOR_KIND_LEVEL: + aqi = data[f"aqi{self._locale}"] + self._state, self._icon = async_get_pollutant_level_info(aqi) + elif self._kind == SENSOR_KIND_AQI: + self._state = data[f"aqi{self._locale}"] + elif self._kind == SENSOR_KIND_POLLUTANT: + symbol = data[f"main{self._locale}"] + self._state = async_get_pollutant_label(symbol) + self._attrs.update( + { + ATTR_POLLUTANT_SYMBOL: symbol, + ATTR_POLLUTANT_UNIT: async_get_pollutant_unit(symbol), + } + ) + + # Displaying the geography on the map relies upon putting the latitude/longitude + # in the entity attributes with "latitude" and "longitude" as the keys. + # Conversely, we can hide the location on the map by using other keys, like + # "lati" and "long". + # + # We use any coordinates in the config entry and, in the case of a geography by + # name, we fall back to the latitude longitude provided in the coordinator data: + latitude = self._config_entry.data.get( + CONF_LATITUDE, + self.coordinator.data["location"]["coordinates"][1], + ) + longitude = self._config_entry.data.get( + CONF_LONGITUDE, + self.coordinator.data["location"]["coordinates"][0], + ) + + if self._config_entry.options[CONF_SHOW_ON_MAP]: + self._attrs[ATTR_LATITUDE] = latitude + self._attrs[ATTR_LONGITUDE] = longitude + self._attrs.pop("lati", None) + self._attrs.pop("long", None) + else: + self._attrs["lati"] = latitude + self._attrs["long"] = longitude + self._attrs.pop(ATTR_LATITUDE, None) + self._attrs.pop(ATTR_LONGITUDE, None) + + +class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): + """Define an AirVisual sensor related to a Node/Pro unit.""" + + def __init__(self, coordinator, kind, name, device_class, unit): + """Initialize.""" + super().__init__(coordinator) + + self._device_class = device_class + self._kind = kind + self._name = name + self._state = None + self._unit = unit + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @property + def device_info(self): + """Return device registry information for this entity.""" + return { + "identifiers": {(DOMAIN, self.coordinator.data["serial_number"])}, + "name": self.coordinator.data["settings"]["node_name"], + "manufacturer": "AirVisual", + "model": f'{self.coordinator.data["status"]["model"]}', + "sw_version": ( + f'Version {self.coordinator.data["status"]["system_version"]}' + f'{self.coordinator.data["status"]["app_version"]}' + ), + } + + @property + def name(self): + """Return the name.""" + node_name = self.coordinator.data["settings"]["node_name"] + return f"{node_name} Node/Pro: {self._name}" + + @property + def state(self): + """Return the state.""" + return self._state + + @property + def unique_id(self): + """Return a unique, Home Assistant friendly identifier for this entity.""" + return f"{self.coordinator.data['serial_number']}_{self._kind}" + + @callback + def update_from_latest_data(self): + """Update the entity from the latest data.""" + if self._kind == SENSOR_KIND_BATTERY_LEVEL: + self._state = self.coordinator.data["status"]["battery"] + elif self._kind == SENSOR_KIND_HUMIDITY: + self._state = self.coordinator.data["measurements"].get("humidity") + elif self._kind == SENSOR_KIND_TEMPERATURE: + self._state = self.coordinator.data["measurements"].get("temperature_C") diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/strings.json new file mode 100644 index 00000000000..8d2dce85a17 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/strings.json @@ -0,0 +1,63 @@ +{ + "config": { + "step": { + "geography_by_coords": { + "title": "Configure a Geography", + "description": "Use the AirVisual cloud API to monitor a latitude/longitude.", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]" + } + }, + "geography_by_name": { + "title": "Configure a Geography", + "description": "Use the AirVisual cloud API to monitor a city/state/country.", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "city": "City", + "country": "Country", + "state": "state" + } + }, + "node_pro": { + "title": "Configure an AirVisual Node/Pro", + "description": "Monitor a personal AirVisual unit. The password can be retrieved from the unit's UI.", + "data": { + "ip_address": "[%key:common::config_flow::data::host%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, + "reauth_confirm": { + "title": "Re-authenticate AirVisual", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + }, + "user": { + "title": "Configure AirVisual", + "description": "Pick what type of AirVisual data you want to monitor." + } + }, + "error": { + "general_error": "[%key:common::config_flow::error::unknown%]", + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "location_not_found": "Location not found", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%] or Node/Pro ID is already registered.", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + }, + "options": { + "step": { + "init": { + "title": "Configure AirVisual", + "data": { + "show_on_map": "Show monitored geography on the map" + } + } + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/ar.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/ar.json new file mode 100644 index 00000000000..771d88e8434 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/ar.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "geography_by_name": { + "data": { + "country": "\u0627\u0644\u062f\u0648\u0644\u0629" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/bg.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/bg.json new file mode 100644 index 00000000000..7e463418576 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "geography_by_name": { + "data": { + "city": "\u0413\u0440\u0430\u0434", + "country": "\u0421\u0442\u0440\u0430\u043d\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/ca.json new file mode 100644 index 00000000000..0440189cdb9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/ca.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada o el Node/Pro ID ja est\u00e0 registrat.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "general_error": "Error inesperat", + "invalid_api_key": "Clau API inv\u00e0lida", + "location_not_found": "No s'ha trobat la ubicaci\u00f3" + }, + "step": { + "geography_by_coords": { + "data": { + "api_key": "Clau API", + "latitude": "Latitud", + "longitude": "Longitud" + }, + "description": "Utilitza l'API d'AirVisual per monitoritzar una latitud/longitud.", + "title": "Configura una ubicaci\u00f3 geogr\u00e0fica" + }, + "geography_by_name": { + "data": { + "api_key": "Clau API", + "city": "Ciutat", + "country": "Pa\u00eds", + "state": "Estat" + }, + "description": "Utilitza l'API d'AirVisual per monitoritzar un/a ciutat/estat/pa\u00eds", + "title": "Configura una ubicaci\u00f3 geogr\u00e0fica" + }, + "node_pro": { + "data": { + "ip_address": "Amfitri\u00f3", + "password": "Contrasenya" + }, + "description": "Monitoritza una unitat personal d'AirVisual. Pots obtenir la contrasenya des de la interf\u00edcie d'usuari (UI) de la unitat.", + "title": "Configuraci\u00f3 d'AirVisual Node/Pro" + }, + "reauth_confirm": { + "data": { + "api_key": "Clau API" + }, + "title": "Re-autenticaci\u00f3 amb AirVisual" + }, + "user": { + "description": "Tria quin tipus de dades d'AirVisual vols monitoritzar.", + "title": "Configura AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Mostra al mapa l'\u00e0rea geogr\u00e0fica monitoritzada" + }, + "title": "Configuraci\u00f3 d'AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/cs.json new file mode 100644 index 00000000000..4fd193e6ddc --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/cs.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno nebo ID uzlu/Pro je ji\u017e zaregistrov\u00e1no.", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "general_error": "Neo\u010dek\u00e1van\u00e1 chyba", + "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API" + }, + "step": { + "geography_by_coords": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka" + } + }, + "geography_by_name": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "city": "M\u011bsto", + "country": "Zem\u011b" + } + }, + "node_pro": { + "data": { + "ip_address": "Hostitel", + "password": "Heslo" + }, + "title": "Nastaven\u00ed AirVisual Node/Pro" + }, + "reauth_confirm": { + "data": { + "api_key": "Kl\u00ed\u010d API" + }, + "title": "Znovu ov\u011b\u0159it AirVisual" + }, + "user": { + "description": "Vyberte, jak\u00fd typ dat AirVisual chcete sledovat.", + "title": "Nastaven\u00ed AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Uk\u00e1zat monitorovanou oblast na map\u011b" + }, + "title": "Nastavte AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/de.json new file mode 100644 index 00000000000..588d69f96fe --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/de.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "Diese Koordinaten oder Node/Pro ID sind bereits registriert.", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "general_error": "Unerwarteter Fehler", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", + "location_not_found": "Standort nicht gefunden" + }, + "step": { + "geography_by_coords": { + "data": { + "api_key": "API-Schl\u00fcssel", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad" + }, + "description": "Verwende die AirVisual Cloud API, um einen L\u00e4ngengrad/Breitengrad zu \u00fcberwachen.", + "title": "Konfiguriere einen Standort" + }, + "geography_by_name": { + "data": { + "api_key": "API-Schl\u00fcssel", + "city": "Stadt", + "country": "Land", + "state": "Bundesland" + }, + "description": "Verwende die AirVisual Cloud API, um ein(e) Stadt/Bundesland/Land zu \u00fcberwachen.", + "title": "Konfiguriere einen Standort" + }, + "node_pro": { + "data": { + "ip_address": "Host", + "password": "Passwort" + }, + "description": "\u00dcberwachen Sie eine pers\u00f6nliche AirVisual-Einheit. Das Passwort kann von der Benutzeroberfl\u00e4che des Ger\u00e4ts abgerufen werden.", + "title": "Konfigurieren Sie einen AirVisual Node/Pro" + }, + "reauth_confirm": { + "data": { + "api_key": "API-Key" + }, + "title": "AirVisual erneut authentifizieren" + }, + "user": { + "description": "W\u00e4hlen Sie aus, welche Art von AirVisual-Daten Sie \u00fcberwachen m\u00f6chten.", + "title": "Konfigurieren Sie AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Zeigen Sie die \u00fcberwachte Geografie auf der Karte an" + }, + "title": "Konfigurieren Sie AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/el.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/el.json new file mode 100644 index 00000000000..04b238a916d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/el.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/en.json new file mode 100644 index 00000000000..1e3cb59a520 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/en.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "Location is already configured or Node/Pro ID is already registered.", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "general_error": "Unexpected error", + "invalid_api_key": "Invalid API key", + "location_not_found": "Location not found" + }, + "step": { + "geography_by_coords": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude" + }, + "description": "Use the AirVisual cloud API to monitor a latitude/longitude.", + "title": "Configure a Geography" + }, + "geography_by_name": { + "data": { + "api_key": "API Key", + "city": "City", + "country": "Country", + "state": "state" + }, + "description": "Use the AirVisual cloud API to monitor a city/state/country.", + "title": "Configure a Geography" + }, + "node_pro": { + "data": { + "ip_address": "Host", + "password": "Password" + }, + "description": "Monitor a personal AirVisual unit. The password can be retrieved from the unit's UI.", + "title": "Configure an AirVisual Node/Pro" + }, + "reauth_confirm": { + "data": { + "api_key": "API Key" + }, + "title": "Re-authenticate AirVisual" + }, + "user": { + "description": "Pick what type of AirVisual data you want to monitor.", + "title": "Configure AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Show monitored geography on the map" + }, + "title": "Configure AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/es-419.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/es-419.json new file mode 100644 index 00000000000..b0022391e62 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/es-419.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Estas coordenadas ya han sido registradas." + }, + "error": { + "general_error": "Se ha producido un error desconocido.", + "invalid_api_key": "Se proporciona una clave de API no v\u00e1lida.", + "location_not_found": "Ubicaci\u00f3n no encontrada" + }, + "step": { + "geography_by_coords": { + "description": "Utilice la API en la nube de AirVisual para monitorear una latitud / longitud.", + "title": "Configurar una geograf\u00eda" + }, + "node_pro": { + "data": { + "ip_address": "Direcci\u00f3n IP/nombre de host de la unidad", + "password": "Contrase\u00f1a de la unidad" + }, + "description": "Monitoree una unidad AirVisual personal. La contrase\u00f1a se puede recuperar de la interfaz de usuario de la unidad.", + "title": "Configurar un AirVisual Node/Pro" + }, + "user": { + "description": "Monitoree la calidad del aire en una ubicaci\u00f3n geogr\u00e1fica.", + "title": "Configurar AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Mostrar geograf\u00eda monitoreada en el mapa" + }, + "title": "Configurar AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/es.json new file mode 100644 index 00000000000..6e7ea6e6903 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/es.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada o el Nodo/Pro ID ya est\u00e1 registrado.", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "general_error": "Se ha producido un error desconocido.", + "invalid_api_key": "Se proporciona una clave API no v\u00e1lida.", + "location_not_found": "Ubicaci\u00f3n no encontrada" + }, + "step": { + "geography_by_coords": { + "data": { + "api_key": "Clave API", + "latitude": "Latitud", + "longitude": "Longitud" + }, + "description": "Utilice la API de la nube de AirVisual para supervisar una latitud/longitud.", + "title": "Configurar una geograf\u00eda" + }, + "geography_by_name": { + "data": { + "api_key": "Clave API", + "city": "Ciudad", + "country": "Pa\u00eds", + "state": "estado" + }, + "description": "Utilice la API de la nube de AirVisual para supervisar una ciudad/estado/pa\u00eds.", + "title": "Configurar una geograf\u00eda" + }, + "node_pro": { + "data": { + "ip_address": "Direcci\u00f3n IP/Nombre de host de la Unidad", + "password": "Contrase\u00f1a" + }, + "description": "Monitorizar una unidad personal AirVisual. La contrase\u00f1a puede ser recuperada desde la interfaz de la unidad.", + "title": "Configurar un AirVisual Node/Pro" + }, + "reauth_confirm": { + "data": { + "api_key": "Clave API" + }, + "title": "Volver a autenticar AirVisual" + }, + "user": { + "description": "Elige qu\u00e9 tipo de datos de AirVisual quieres monitorizar.", + "title": "Configurar AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Mostrar geograf\u00eda monitorizada en el mapa" + }, + "title": "Configurar AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/et.json new file mode 100644 index 00000000000..45490bb63fe --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/et.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "Asukoht on juba m\u00e4\u00e4ratud v\u00f5i Node/Pro ID on juba registreeritud.", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "general_error": "Tundmatu viga", + "invalid_api_key": "Vale API v\u00f5ti", + "location_not_found": "Asukohta ei leitud" + }, + "step": { + "geography_by_coords": { + "data": { + "api_key": "API v\u00f5ti", + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad" + }, + "description": "Kasuta AirVisual pilve API-t pikkus/laiuskraadi j\u00e4lgimiseks.", + "title": "Seadista Geography sidumine" + }, + "geography_by_name": { + "data": { + "api_key": "API v\u00f5ti", + "city": "Linn", + "country": "Riik", + "state": "olek" + }, + "description": "Kasuta AirVisual pilve API-t linna/osariigi/riigi j\u00e4lgimiseks.", + "title": "Seadista Geography sidumine" + }, + "node_pro": { + "data": { + "ip_address": "\u00dcksuse IP-aadress / hostinimi", + "password": "Salas\u00f5na" + }, + "description": "J\u00e4lgige isiklikku AirVisual-seadet. Parooli saab hankida seadme kasutajaliidese kaudu.", + "title": "Seadistage AirVisual Node / Pro" + }, + "reauth_confirm": { + "data": { + "api_key": "API v\u00f5ti" + }, + "title": "Taastuvasta AirVisual" + }, + "user": { + "description": "Vali millist t\u00fc\u00fcpi AirVisuali andmeid soovid j\u00e4lgida.", + "title": "Seadista AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "N\u00e4ita j\u00e4lgitavat asukohta kaardil" + }, + "title": "Seadista AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/fi.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/fi.json new file mode 100644 index 00000000000..044d7688551 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/fi.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "general_error": "Tapahtui tuntematon virhe." + }, + "step": { + "node_pro": { + "data": { + "password": "Salasana" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/fr.json new file mode 100644 index 00000000000..510bf8597d1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/fr.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "Cette cl\u00e9 API est d\u00e9j\u00e0 utilis\u00e9e.", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "general_error": "Erreur inattendue", + "invalid_api_key": "Cl\u00e9 API invalide", + "location_not_found": "Emplacement introuvable" + }, + "step": { + "geography_by_coords": { + "data": { + "api_key": "Clef d'API", + "latitude": "Latitude", + "longitude": "Longitude" + }, + "description": "Utilisez l'API cloud AirVisual pour surveiller une latitude / longitude.", + "title": "Configurer un lieu g\u00e9ographique" + }, + "geography_by_name": { + "data": { + "api_key": "Clef d'API", + "city": "Ville", + "country": "Pays", + "state": "Etat" + }, + "description": "Utilisez l'API cloud AirVisual pour surveiller une ville / un \u00e9tat / un pays.", + "title": "Configurer un lieu g\u00e9ographique" + }, + "node_pro": { + "data": { + "ip_address": "H\u00f4te", + "password": "Mot de passe" + }, + "description": "Surveillez une unit\u00e9 personnelle AirVisual. Le mot de passe peut \u00eatre r\u00e9cup\u00e9r\u00e9 dans l'interface utilisateur de l'unit\u00e9.", + "title": "Configurer un noeud AirVisual Pro" + }, + "reauth_confirm": { + "data": { + "api_key": "Cl\u00e9 d'API" + }, + "title": "R\u00e9-authentifier AirVisual" + }, + "user": { + "description": "Choisissez le type de donn\u00e9es AirVisual que vous souhaitez surveiller.", + "title": "Configurer AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Afficher la g\u00e9ographie surveill\u00e9e sur la carte" + }, + "title": "Configurer AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/he.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/he.json new file mode 100644 index 00000000000..7fc0c2983df --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/he.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9 \u05e1\u05d5\u05e4\u05e7" + }, + "step": { + "node_pro": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/hi.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/hi.json new file mode 100644 index 00000000000..ee03f27ccc0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/hi.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "general_error": "\u0915\u094b\u0908 \u0905\u091c\u094d\u091e\u093e\u0924 \u0924\u094d\u0930\u0941\u091f\u093f \u0925\u0940\u0964" + }, + "step": { + "node_pro": { + "data": { + "ip_address": "\u0907\u0915\u093e\u0908 \u0915\u0947 \u0906\u0908\u092a\u0940 \u092a\u0924\u0947/\u0939\u094b\u0938\u094d\u091f\u0928\u093e\u092e", + "password": "\u0907\u0915\u093e\u0908 \u092a\u093e\u0938\u0935\u0930\u094d\u0921" + }, + "description": "\u090f\u0915 \u0935\u094d\u092f\u0915\u094d\u0924\u093f\u0917\u0924 \u090f\u092f\u0930\u0935\u093f\u091c\u0941\u0905\u0932 \u0907\u0915\u093e\u0908 \u0915\u0940 \u0928\u093f\u0917\u0930\u093e\u0928\u0940 \u0915\u0930\u0947\u0902\u0964 \u092a\u093e\u0938\u0935\u0930\u094d\u0921 \u092f\u0942\u0928\u093f\u091f \u0915\u0947 \u092f\u0942\u0906\u0908 \u0938\u0947 \u092a\u094d\u0930\u093e\u092a\u094d\u0924 \u0915\u093f\u092f\u093e \u091c\u093e \u0938\u0915\u0924\u093e \u0939\u0948\u0964", + "title": "\u090f\u092f\u0930\u0935\u093f\u091c\u0941\u0905\u0932 \u0928\u094b\u0921 \u092a\u094d\u0930\u094b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/hu.json new file mode 100644 index 00000000000..704ce33ab67 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/hu.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van vagy a Node/Pro azonos\u00edt\u00f3 m\u00e1r regisztr\u00e1lva van.", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "general_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs" + }, + "step": { + "geography_by_coords": { + "data": { + "api_key": "API kulcs", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g" + } + }, + "geography_by_name": { + "data": { + "api_key": "API kulcs", + "city": "V\u00e1ros", + "country": "Orsz\u00e1g" + } + }, + "node_pro": { + "data": { + "ip_address": "Hoszt", + "password": "Jelsz\u00f3" + } + }, + "reauth_confirm": { + "data": { + "api_key": "API kulcs" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/id.json new file mode 100644 index 00000000000..6fcd6eb5410 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/id.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "Lokasi sudah dikonfigurasi atau ID Node/Pro sudah terdaftar.", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "general_error": "Kesalahan yang tidak diharapkan", + "invalid_api_key": "Kunci API tidak valid", + "location_not_found": "Lokasi tidak ditemukan" + }, + "step": { + "geography_by_coords": { + "data": { + "api_key": "Kunci API", + "latitude": "Lintang", + "longitude": "Bujur" + }, + "description": "Gunakan API cloud AirVisual untuk memantau satu pasang lintang/bujur.", + "title": "Konfigurasikan Lokasi Geografi" + }, + "geography_by_name": { + "data": { + "api_key": "Kunci API", + "city": "Kota", + "country": "Negara", + "state": "negara bagian" + }, + "description": "Gunakan API cloud AirVisual untuk memantau kota/negara bagian/negara.", + "title": "Konfigurasikan Lokasi Geografi" + }, + "node_pro": { + "data": { + "ip_address": "Host", + "password": "Kata Sandi" + }, + "description": "Pantau unit AirVisual pribadi. Kata sandi dapat diambil dari antarmuka unit.", + "title": "Konfigurasikan AirVisual Node/Pro" + }, + "reauth_confirm": { + "data": { + "api_key": "Kunci API" + }, + "title": "Autentikasi Ulang AirVisual" + }, + "user": { + "description": "Pilih jenis data AirVisual yang ingin dipantau.", + "title": "Konfigurasikan AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Tampilkan lokasi geografi yang dipantau pada peta" + }, + "title": "Konfigurasikan AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/it.json new file mode 100644 index 00000000000..6201c6f19d9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/it.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "La posizione \u00e8 gi\u00e0 configurata o Node/Pro ID sono gi\u00e0 registrati.", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "general_error": "Errore imprevisto", + "invalid_api_key": "Chiave API non valida", + "location_not_found": "Posizione non trovata" + }, + "step": { + "geography_by_coords": { + "data": { + "api_key": "Chiave API", + "latitude": "Latitudine", + "longitude": "Logitudine" + }, + "description": "Usa l'API cloud di AirVisual per monitorare una latitudine/longitudine.", + "title": "Configurare un'area geografica" + }, + "geography_by_name": { + "data": { + "api_key": "Chiave API", + "city": "Citt\u00e0", + "country": "Nazione", + "state": "Stato" + }, + "description": "Usa l'API cloud di AirVisual per monitorare una citt\u00e0/stato/paese.", + "title": "Configurare un'area geografica" + }, + "node_pro": { + "data": { + "ip_address": "Host", + "password": "Password" + }, + "description": "Monitorare un'unit\u00e0 AirVisual personale. La password pu\u00f2 essere recuperata dall'interfaccia utente dell'unit\u00e0.", + "title": "Configurare un AirVisual Node/Pro" + }, + "reauth_confirm": { + "data": { + "api_key": "Chiave API" + }, + "title": "Riautenticare AirVisual" + }, + "user": { + "description": "Scegliere il tipo di dati AirVisual che si desidera monitorare.", + "title": "Configura AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Mostra l'area geografica monitorata sulla mappa" + }, + "title": "Configurare AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/ka.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/ka.json new file mode 100644 index 00000000000..cb01b5d0d14 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/ka.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u10ec\u10d0\u10e0\u10db\u10d0\u10e2\u10d4\u10d1\u10e3\u10da\u10d8 \u10e0\u10d4-\u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API Key" + }, + "title": "AirVisual \u10e0\u10d4-\u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/ko.json new file mode 100644 index 00000000000..ddee51dcb3e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/ko.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "Node/Pro ID\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uac70\ub098 \uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "general_error": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", + "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "location_not_found": "\uc704\uce58\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "step": { + "geography_by_coords": { + "data": { + "api_key": "API \ud0a4", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4" + }, + "description": "AirVisual \ud074\ub77c\uc6b0\ub4dc API\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc704\ub3c4/\uacbd\ub3c4\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4.", + "title": "\uc9c0\ub9ac\uc801 \uc704\uce58 \uad6c\uc131\ud558\uae30" + }, + "geography_by_name": { + "data": { + "api_key": "API \ud0a4", + "city": "\ub3c4\uc2dc", + "country": "\uad6d\uac00", + "state": "\uc8fc" + }, + "description": "AirVisual \ud074\ub77c\uc6b0\ub4dc API\ub97c \uc0ac\uc6a9\ud558\uc5ec \ub3c4\uc2dc/\uc8fc/\uad6d\uac00\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4.", + "title": "\uc9c0\ub9ac\uc801 \uc704\uce58 \uad6c\uc131\ud558\uae30" + }, + "node_pro": { + "data": { + "ip_address": "\ud638\uc2a4\ud2b8", + "password": "\ube44\ubc00\ubc88\ud638" + }, + "description": "\uc0ac\uc6a9\uc790\uc758 AirVisual \uae30\uae30\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4. \uae30\uae30\uc758 UI \uc5d0\uc11c \ube44\ubc00\ubc88\ud638\ub97c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "AirVisual Node/Pro \uad6c\uc131\ud558\uae30" + }, + "reauth_confirm": { + "data": { + "api_key": "API \ud0a4" + }, + "title": "AirVisual \uc7ac\uc778\uc99d\ud558\uae30" + }, + "user": { + "description": "\ubaa8\ub2c8\ud130\ub9c1\ud560 AirVisual \ub370\uc774\ud130 \uc720\ud615\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", + "title": "AirVisual \uad6c\uc131\ud558\uae30" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "\uc9c0\ub3c4\uc5d0 \ubaa8\ub2c8\ud130\ub9c1\ub41c \uc9c0\ub9ac \uc815\ubcf4 \ud45c\uc2dc" + }, + "title": "AirVisual \uad6c\uc131\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/lb.json new file mode 100644 index 00000000000..12906b45277 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/lb.json @@ -0,0 +1,51 @@ +{ + "config": { + "abort": { + "already_configured": "Standuert ass scho konfigur\u00e9iert oder Node/Pro ID ass scho registr\u00e9iert.", + "reauth_successful": "Re-authentifikatioun war erfollegr\u00e4ich" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "general_error": "Onerwaarte Feeler", + "invalid_api_key": "Ong\u00ebltegen API Schl\u00ebssel", + "location_not_found": "Standuert net fonnt." + }, + "step": { + "geography_by_name": { + "data": { + "city": "Stad", + "country": "Land", + "state": "Kanton" + } + }, + "node_pro": { + "data": { + "ip_address": "Host", + "password": "Passwuert" + }, + "description": "Pers\u00e9inlech Airvisual Unit\u00e9it iwwerwaachen. Passwuert kann vum UI vum Apparat ausgelies ginn.", + "title": "Airvisual Node/Pro ariichten" + }, + "reauth_confirm": { + "data": { + "api_key": "API Schl\u00ebssel" + }, + "title": "AirVisual re-authentifiz\u00e9ieren" + }, + "user": { + "description": "Typ vun Airvisual Donn\u00e9\u00eb fir d'Iwwerwachung auswielen.", + "title": "AirVisual konfigur\u00e9ieren" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Iwwerwaachte Geografie op der Kaart uweisen" + }, + "title": "Airvisual ariichten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/nl.json new file mode 100644 index 00000000000..ddbcc6e6009 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/nl.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "Locatie is al geconfigureerd. of Node/Pro IDis al geregistreerd.", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "general_error": "Er is een onbekende fout opgetreden.", + "invalid_api_key": "Ongeldige API-sleutel", + "location_not_found": "Locatie niet gevonden" + }, + "step": { + "geography_by_coords": { + "data": { + "api_key": "API-sleutel", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad" + }, + "description": "Gebruik de AirVisual-cloud-API om een lengte- / breedtegraad te bewaken.", + "title": "Configureer een geografie" + }, + "geography_by_name": { + "data": { + "api_key": "API-sleutel", + "city": "Stad", + "country": "Land", + "state": "staat" + }, + "description": "Gebruik de AirVisual-cloud-API om een stad/staat/land te bewaken.", + "title": "Configureer een geografie" + }, + "node_pro": { + "data": { + "ip_address": "IP adres/hostname van component", + "password": "Wachtwoord van component" + }, + "description": "Monitor een persoonlijke AirVisual-eenheid. Het wachtwoord kan worden opgehaald uit de gebruikersinterface van het apparaat.", + "title": "Configureer een AirVisual Node / Pro" + }, + "reauth_confirm": { + "data": { + "api_key": "API-sleutel" + }, + "title": "Verifieer AirVisual opnieuw" + }, + "user": { + "description": "Kies welk type AirVisual-gegevens u wilt bewaken.", + "title": "Configureer AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Toon gecontroleerde geografie op de kaart" + }, + "title": "Configureer AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/no.json new file mode 100644 index 00000000000..d4ca80d4805 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/no.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "Plasseringen er allerede konfigurert eller Node / Pro ID er allerede registrert.", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "general_error": "Uventet feil", + "invalid_api_key": "Ugyldig API-n\u00f8kkel", + "location_not_found": "Stedet ble ikke funnet" + }, + "step": { + "geography_by_coords": { + "data": { + "api_key": "API-n\u00f8kkel", + "latitude": "Breddegrad", + "longitude": "Lengdegrad" + }, + "description": "Bruk AirVisual cloud API til \u00e5 overv\u00e5ke en breddegrad/lengdegrad.", + "title": "Konfigurer en Geography" + }, + "geography_by_name": { + "data": { + "api_key": "API-n\u00f8kkel", + "city": "By", + "country": "Land", + "state": "stat" + }, + "description": "Bruk AirVisual cloud API til \u00e5 overv\u00e5ke en by/stat/land.", + "title": "Konfigurer en Geography" + }, + "node_pro": { + "data": { + "ip_address": "Vert", + "password": "Passord" + }, + "description": "Overv\u00e5ke en personlig AirVisual-enhet. Passordet kan hentes fra enhetens brukergrensesnitt.", + "title": "Konfigurer en AirVisual Node / Pro" + }, + "reauth_confirm": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "title": "Godkjenne integrering p\u00e5 nytt" + }, + "user": { + "description": "Velg hvilken type AirVisual-data du vil overv\u00e5ke.", + "title": "Konfigurer AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Vis overv\u00e5ket geografi p\u00e5 kartet" + }, + "title": "Konfigurer AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/pl.json new file mode 100644 index 00000000000..26883f514cd --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/pl.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "Lokalizacja jest ju\u017c skonfigurowana lub ID Node/Pro jest ju\u017c zarejestrowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "general_error": "Nieoczekiwany b\u0142\u0105d", + "invalid_api_key": "Nieprawid\u0142owy klucz API", + "location_not_found": "Nie znaleziono lokalizacji" + }, + "step": { + "geography_by_coords": { + "data": { + "api_key": "Klucz API", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna" + }, + "description": "U\u017cyj API chmury AirVisual do monitorowania szeroko\u015bci/d\u0142ugo\u015bci geograficznej.", + "title": "Konfiguracja Geography" + }, + "geography_by_name": { + "data": { + "api_key": "Klucz API", + "city": "Miasto", + "country": "Kraj", + "state": "Stan" + }, + "description": "U\u017cyj API chmury AirVisual do monitorowania miasta/stanu/kraju.", + "title": "Konfiguracja Geography" + }, + "node_pro": { + "data": { + "ip_address": "Nazwa hosta lub adres IP", + "password": "Has\u0142o" + }, + "description": "Monitoruj jednostk\u0119 AirVisual. Has\u0142o mo\u017cna odzyska\u0107 z interfejsu u\u017cytkownika urz\u0105dzenia.", + "title": "Konfiguracja AirVisual Node/Pro" + }, + "reauth_confirm": { + "data": { + "api_key": "Klucz API" + }, + "title": "Ponownie uwierzytelnij integracj\u0119" + }, + "user": { + "description": "Wybierz, kt\u00f3re dane AirVisual chcesz monitorowa\u0107.", + "title": "Konfiguracja AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Wy\u015bwietlaj encje na mapie" + }, + "title": "Konfiguracja AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/pt-BR.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/pt-BR.json new file mode 100644 index 00000000000..733411f2465 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/pt-BR.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "general_error": "Ocorreu um erro desconhecido.", + "invalid_api_key": "Chave de API fornecida \u00e9 inv\u00e1lida." + }, + "step": { + "node_pro": { + "data": { + "password": "Senha" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/pt.json new file mode 100644 index 00000000000..cc1c500946d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/pt.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada ou Node/Pro ID j\u00e1 est\u00e1 registrado.", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "general_error": "Erro inesperado", + "invalid_api_key": "Chave de API inv\u00e1lida" + }, + "step": { + "node_pro": { + "data": { + "ip_address": "Servidor", + "password": "Palavra-passe" + } + }, + "reauth_confirm": { + "data": { + "api_key": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/ru.json new file mode 100644 index 00000000000..4f0073d3132 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/ru.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u041b\u0438\u0431\u043e \u044d\u0442\u043e\u0442 Node / Pro ID \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.", + "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": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "general_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "location_not_found": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e." + }, + "step": { + "geography_by_coords": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430" + }, + "description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0448\u0438\u0440\u043e\u0442\u044b/\u0434\u043e\u043b\u0433\u043e\u0442\u044b.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f" + }, + "geography_by_name": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "city": "\u0413\u043e\u0440\u043e\u0434", + "country": "\u0421\u0442\u0440\u0430\u043d\u0430", + "state": "\u0448\u0442\u0430\u0442" + }, + "description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0433\u043e\u0440\u043e\u0434\u0430/\u0448\u0442\u0430\u0442\u0430/\u0441\u0442\u0440\u0430\u043d\u044b.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f" + }, + "node_pro": { + "data": { + "ip_address": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 AirVisual. \u041f\u0430\u0440\u043e\u043b\u044c \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0432 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 AirVisual Node / Pro" + }, + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f" + }, + "user": { + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u0434\u0430\u043d\u043d\u044b\u0445 AirVisual, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c.", + "title": "AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0435\u043c\u0443\u044e \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u043d\u0430 \u043a\u0430\u0440\u0442\u0435" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/sl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/sl.json new file mode 100644 index 00000000000..fc611a1589e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/sl.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Te koordinate so \u017ee registrirane." + }, + "error": { + "general_error": "Pri\u0161lo je do neznane napake.", + "invalid_api_key": "Vpisan neveljaven API klju\u010d" + }, + "step": { + "node_pro": { + "data": { + "ip_address": "IP naslov/ime gostitelja enote", + "password": "Geslo enote" + }, + "description": "Spremljajte osebno napravo AirVisual. Geslo je mogo\u010de pridobiti iz uporabni\u0161kega vmesnika enote.", + "title": "Konfigurirajte AirVisual Node/Pro" + }, + "user": { + "description": "Spremljajte kakovost zraka na zemljepisni lokaciji.", + "title": "Nastavite AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Prika\u017ei nadzorovano obmo\u010dje na zemljevidu" + }, + "title": "Nastavite AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/sv.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/sv.json new file mode 100644 index 00000000000..6a33c0393d9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "general_error": "Ett ok\u00e4nt fel intr\u00e4ffade.", + "invalid_api_key": "Ogiltig API-nyckel" + }, + "step": { + "node_pro": { + "data": { + "ip_address": "Enhets IP-adress / v\u00e4rdnamn", + "password": "Enhetsl\u00f6senord" + } + }, + "user": { + "title": "Konfigurera AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/tr.json new file mode 100644 index 00000000000..6f27841ea13 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/tr.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "general_error": "Beklenmeyen hata", + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", + "location_not_found": "Konum bulunamad\u0131" + }, + "step": { + "geography_by_coords": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam" + }, + "description": "Bir enlem / boylam\u0131 izlemek i\u00e7in AirVisual bulut API'sini kullan\u0131n.", + "title": "Bir Co\u011frafyay\u0131 Yap\u0131land\u0131rma" + }, + "geography_by_name": { + "data": { + "api_key": "API Anahtar\u0131", + "city": "\u015eehir", + "country": "\u00dclke", + "state": "durum" + }, + "title": "Bir Co\u011frafyay\u0131 Yap\u0131land\u0131rma" + }, + "node_pro": { + "data": { + "ip_address": "Ana Bilgisayar", + "password": "Parola" + }, + "description": "Ki\u015fisel bir AirVisual \u00fcnitesini izleyin. Parola, \u00fcnitenin kullan\u0131c\u0131 aray\u00fcz\u00fcnden al\u0131nabilir." + }, + "reauth_confirm": { + "data": { + "api_key": "API Anahtar\u0131" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "AirVisual'\u0131 yap\u0131land\u0131r\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/uk.json new file mode 100644 index 00000000000..4a4ea6c8b90 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/uk.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435. \u0410\u0431\u043e \u0446\u0435\u0439 Node / Pro ID \u0432\u0436\u0435 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u043e\u0432\u0430\u043d\u0438\u0439.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "general_error": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430", + "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API" + }, + "step": { + "node_pro": { + "data": { + "ip_address": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e AirVisual. \u041f\u0430\u0440\u043e\u043b\u044c \u043c\u043e\u0436\u043d\u0430 \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AirVisual Node / Pro" + }, + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0444\u0456\u043b\u044e" + }, + "user": { + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0438\u043f \u0434\u0430\u043d\u0438\u0445 AirVisual, \u044f\u043a\u0438\u0439 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0432\u0430\u0442\u0438.", + "title": "AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0432\u0430\u043d\u0443 \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u043d\u0430 \u043a\u0430\u0440\u0442\u0456" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/zh-Hans.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/zh-Hans.json new file mode 100644 index 00000000000..2941dfd9383 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/zh-Hant.json new file mode 100644 index 00000000000..172f57de938 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/airvisual/translations/zh-Hant.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "Node/Pro ID \u5df2\u8a3b\u518a\u6216\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "general_error": "\u672a\u9810\u671f\u932f\u8aa4", + "invalid_api_key": "API \u5bc6\u9470\u7121\u6548", + "location_not_found": "\u627e\u4e0d\u5230\u5730\u9ede" + }, + "step": { + "geography_by_coords": { + "data": { + "api_key": "API \u5bc6\u9470", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6" + }, + "description": "\u4f7f\u7528 AirVisual \u96f2\u7aef API \u4ee5\u76e3\u63a7\u7d93\u5ea6/\u7def\u5ea6\u3002", + "title": "\u8a2d\u5b9a\u5730\u7406\u5ea7\u6a19" + }, + "geography_by_name": { + "data": { + "api_key": "API \u5bc6\u9470", + "city": "\u57ce\u5e02", + "country": "\u570b\u5bb6", + "state": "\u5dde" + }, + "description": "\u4f7f\u7528 AirVisual \u96f2\u7aef API \u4ee5\u76e3\u63a7\u57ce\u5e02/\u5dde/\u570b\u5bb6\u3002", + "title": "\u8a2d\u5b9a\u5730\u7406\u5ea7\u6a19" + }, + "node_pro": { + "data": { + "ip_address": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc" + }, + "description": "\u76e3\u63a7\u500b\u4eba AirVisual \u88dd\u7f6e\uff0c\u5bc6\u78bc\u53ef\u4ee5\u900f\u904e\u88dd\u7f6e UI \u7372\u5f97\u3002", + "title": "\u8a2d\u5b9a AirVisual Node/Pro" + }, + "reauth_confirm": { + "data": { + "api_key": "API \u5bc6\u9470" + }, + "title": "\u91cd\u65b0\u8a8d\u8b49 AirVisual" + }, + "user": { + "description": "\u9078\u64c7\u6240\u8981\u76e3\u63a7\u7684 AirVisual \u8cc7\u6599\u985e\u578b\u3002", + "title": "\u8a2d\u5b9a AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "\u65bc\u5730\u5716\u4e0a\u986f\u793a\u76e3\u63a7\u4f4d\u7f6e\u3002" + }, + "title": "\u8a2d\u5b9a AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aladdin_connect/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aladdin_connect/__init__.py new file mode 100644 index 00000000000..90196616dc5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aladdin_connect/__init__.py @@ -0,0 +1 @@ +"""The aladdin_connect component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aladdin_connect/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aladdin_connect/const.py new file mode 100644 index 00000000000..7bfea738cef --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aladdin_connect/const.py @@ -0,0 +1,19 @@ +"""Platform for the Aladdin Connect cover component.""" +from __future__ import annotations + +from typing import Final + +from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN +from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING + +NOTIFICATION_ID: Final = "aladdin_notification" +NOTIFICATION_TITLE: Final = "Aladdin Connect Cover Setup" + +STATES_MAP: Final[dict[str, str]] = { + "open": STATE_OPEN, + "opening": STATE_OPENING, + "closed": STATE_CLOSED, + "closing": STATE_CLOSING, +} + +SUPPORTED_FEATURES: Final = SUPPORT_OPEN | SUPPORT_CLOSE diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aladdin_connect/cover.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aladdin_connect/cover.py new file mode 100644 index 00000000000..d4ae9cbb2fd --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aladdin_connect/cover.py @@ -0,0 +1,120 @@ +"""Platform for the Aladdin Connect cover component.""" +from __future__ import annotations + +import logging +from typing import Any, Final + +from aladdin_connect import AladdinConnectClient +import voluptuous as vol + +from homeassistant.components.cover import ( + PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, + CoverEntity, +) +from homeassistant.const import ( + CONF_PASSWORD, + CONF_USERNAME, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPENING, +) +from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from .const import NOTIFICATION_ID, NOTIFICATION_TITLE, STATES_MAP, SUPPORTED_FEATURES +from .model import DoorDevice + +_LOGGER: Final = logging.getLogger(__name__) + +PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( + {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} +) + + +def setup_platform( + hass: HomeAssistant, + config: ConfigType, + add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Aladdin Connect platform.""" + + username: str = config[CONF_USERNAME] + password: str = config[CONF_PASSWORD] + acc = AladdinConnectClient(username, password) + + try: + if not acc.login(): + raise ValueError("Username or Password is incorrect") + add_entities(AladdinDevice(acc, door) for door in acc.get_doors()) + except (TypeError, KeyError, NameError, ValueError) as ex: + _LOGGER.error("%s", ex) + hass.components.persistent_notification.create( + "Error: {ex}
You will need to restart hass after fixing.", + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID, + ) + + +class AladdinDevice(CoverEntity): + """Representation of Aladdin Connect cover.""" + + def __init__(self, acc: AladdinConnectClient, device: DoorDevice) -> None: + """Initialize the cover.""" + self._acc = acc + self._device_id = device["device_id"] + self._number = device["door_number"] + self._name = device["name"] + self._status = STATES_MAP.get(device["status"]) + + @property + def device_class(self) -> str: + """Define this cover as a garage door.""" + return "garage" + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORTED_FEATURES + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return f"{self._device_id}-{self._number}" + + @property + def name(self) -> str: + """Return the name of the garage door.""" + return self._name + + @property + def is_opening(self) -> bool: + """Return if the cover is opening or not.""" + return self._status == STATE_OPENING + + @property + def is_closing(self) -> bool: + """Return if the cover is closing or not.""" + return self._status == STATE_CLOSING + + @property + def is_closed(self) -> bool | None: + """Return None if status is unknown, True if closed, else False.""" + if self._status is None: + return None + return self._status == STATE_CLOSED + + def close_cover(self, **kwargs: Any) -> None: + """Issue close command to cover.""" + self._acc.close_door(self._device_id, self._number) + + def open_cover(self, **kwargs: Any) -> None: + """Issue open command to cover.""" + self._acc.open_door(self._device_id, self._number) + + def update(self) -> None: + """Update status of cover.""" + acc_status = self._acc.get_door_status(self._device_id, self._number) + self._status = STATES_MAP.get(acc_status) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aladdin_connect/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aladdin_connect/manifest.json new file mode 100644 index 00000000000..b2cc5f6d32c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aladdin_connect/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "aladdin_connect", + "name": "Aladdin Connect", + "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", + "requirements": ["aladdin_connect==0.3"], + "codeowners": [], + "iot_class": "cloud_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aladdin_connect/model.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aladdin_connect/model.py new file mode 100644 index 00000000000..4248f3504fe --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aladdin_connect/model.py @@ -0,0 +1,13 @@ +"""Models for Aladdin connect cover platform.""" +from __future__ import annotations + +from typing import TypedDict + + +class DoorDevice(TypedDict): + """Aladdin door device.""" + + device_id: str + door_number: int + name: str + status: str diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/__init__.py new file mode 100644 index 00000000000..7d9e47fbcbe --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/__init__.py @@ -0,0 +1,196 @@ +"""Component to interface with an alarm control panel.""" +from abc import abstractmethod +from datetime import timedelta +import logging +from typing import final + +import voluptuous as vol + +from homeassistant.const import ( + ATTR_CODE, + ATTR_CODE_FORMAT, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_TRIGGER, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import ( # noqa: F401 + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, + make_entity_service_schema, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent + +from .const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_CUSTOM_BYPASS, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "alarm_control_panel" +SCAN_INTERVAL = timedelta(seconds=30) +ATTR_CHANGED_BY = "changed_by" +FORMAT_TEXT = "text" +FORMAT_NUMBER = "number" +ATTR_CODE_ARM_REQUIRED = "code_arm_required" + +ENTITY_ID_FORMAT = DOMAIN + ".{}" + +ALARM_SERVICE_SCHEMA = make_entity_service_schema({vol.Optional(ATTR_CODE): cv.string}) + + +async def async_setup(hass, config): + """Track states and offer events for sensors.""" + component = hass.data[DOMAIN] = EntityComponent( + logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL + ) + + await component.async_setup(config) + + component.async_register_entity_service( + SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA, "async_alarm_disarm" + ) + component.async_register_entity_service( + SERVICE_ALARM_ARM_HOME, + ALARM_SERVICE_SCHEMA, + "async_alarm_arm_home", + [SUPPORT_ALARM_ARM_HOME], + ) + component.async_register_entity_service( + SERVICE_ALARM_ARM_AWAY, + ALARM_SERVICE_SCHEMA, + "async_alarm_arm_away", + [SUPPORT_ALARM_ARM_AWAY], + ) + component.async_register_entity_service( + SERVICE_ALARM_ARM_NIGHT, + ALARM_SERVICE_SCHEMA, + "async_alarm_arm_night", + [SUPPORT_ALARM_ARM_NIGHT], + ) + component.async_register_entity_service( + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + ALARM_SERVICE_SCHEMA, + "async_alarm_arm_custom_bypass", + [SUPPORT_ALARM_ARM_CUSTOM_BYPASS], + ) + component.async_register_entity_service( + SERVICE_ALARM_TRIGGER, + ALARM_SERVICE_SCHEMA, + "async_alarm_trigger", + [SUPPORT_ALARM_TRIGGER], + ) + + return True + + +async def async_setup_entry(hass, entry): + """Set up a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.data[DOMAIN].async_unload_entry(entry) + + +class AlarmControlPanelEntity(Entity): + """An abstract class for alarm control entities.""" + + @property + def code_format(self): + """Regex for code format or None if no code is required.""" + return None + + @property + def changed_by(self): + """Last change triggered by.""" + return None + + @property + def code_arm_required(self): + """Whether the code is required for arm actions.""" + return True + + def alarm_disarm(self, code=None): + """Send disarm command.""" + raise NotImplementedError() + + async def async_alarm_disarm(self, code=None): + """Send disarm command.""" + await self.hass.async_add_executor_job(self.alarm_disarm, code) + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + raise NotImplementedError() + + async def async_alarm_arm_home(self, code=None): + """Send arm home command.""" + await self.hass.async_add_executor_job(self.alarm_arm_home, code) + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + raise NotImplementedError() + + async def async_alarm_arm_away(self, code=None): + """Send arm away command.""" + await self.hass.async_add_executor_job(self.alarm_arm_away, code) + + def alarm_arm_night(self, code=None): + """Send arm night command.""" + raise NotImplementedError() + + async def async_alarm_arm_night(self, code=None): + """Send arm night command.""" + await self.hass.async_add_executor_job(self.alarm_arm_night, code) + + def alarm_trigger(self, code=None): + """Send alarm trigger command.""" + raise NotImplementedError() + + async def async_alarm_trigger(self, code=None): + """Send alarm trigger command.""" + await self.hass.async_add_executor_job(self.alarm_trigger, code) + + def alarm_arm_custom_bypass(self, code=None): + """Send arm custom bypass command.""" + raise NotImplementedError() + + async def async_alarm_arm_custom_bypass(self, code=None): + """Send arm custom bypass command.""" + await self.hass.async_add_executor_job(self.alarm_arm_custom_bypass, code) + + @property + @abstractmethod + def supported_features(self) -> int: + """Return the list of supported features.""" + + @final + @property + def state_attributes(self): + """Return the state attributes.""" + return { + ATTR_CODE_FORMAT: self.code_format, + ATTR_CHANGED_BY: self.changed_by, + ATTR_CODE_ARM_REQUIRED: self.code_arm_required, + } + + +class AlarmControlPanel(AlarmControlPanelEntity): + """An abstract class for alarm control entities (for backwards compatibility).""" + + def __init_subclass__(cls, **kwargs): + """Print deprecation warning.""" + super().__init_subclass__(**kwargs) + _LOGGER.warning( + "AlarmControlPanel is deprecated, modify %s to extend AlarmControlPanelEntity", + cls.__name__, + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/const.py new file mode 100644 index 00000000000..2844cb286ab --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/const.py @@ -0,0 +1,14 @@ +"""Provides the constants needed for component.""" + +SUPPORT_ALARM_ARM_HOME = 1 +SUPPORT_ALARM_ARM_AWAY = 2 +SUPPORT_ALARM_ARM_NIGHT = 4 +SUPPORT_ALARM_TRIGGER = 8 +SUPPORT_ALARM_ARM_CUSTOM_BYPASS = 16 + +CONDITION_TRIGGERED = "is_triggered" +CONDITION_DISARMED = "is_disarmed" +CONDITION_ARMED_HOME = "is_armed_home" +CONDITION_ARMED_AWAY = "is_armed_away" +CONDITION_ARMED_NIGHT = "is_armed_night" +CONDITION_ARMED_CUSTOM_BYPASS = "is_armed_custom_bypass" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/device_action.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/device_action.py new file mode 100644 index 00000000000..9a55998e929 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/device_action.py @@ -0,0 +1,145 @@ +"""Provides device automations for Alarm control panel.""" +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.const import ( + ATTR_CODE, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_CODE, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_TYPE, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_TRIGGER, +) +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers import entity_registry +import homeassistant.helpers.config_validation as cv + +from . import ATTR_CODE_ARM_REQUIRED, DOMAIN +from .const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) + +ACTION_TYPES = {"arm_away", "arm_home", "arm_night", "disarm", "trigger"} + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + vol.Optional(CONF_CODE): cv.string, + } +) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: + """List device actions for Alarm control panel devices.""" + registry = await entity_registry.async_get_registry(hass) + actions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + state = hass.states.get(entry.entity_id) + + # We need a state or else we can't populate the HVAC and preset modes. + if state is None: + continue + + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + + # Add actions for each entity that belongs to this integration + if supported_features & SUPPORT_ALARM_ARM_AWAY: + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "arm_away", + } + ) + if supported_features & SUPPORT_ALARM_ARM_HOME: + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "arm_home", + } + ) + if supported_features & SUPPORT_ALARM_ARM_NIGHT: + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "arm_night", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "disarm", + } + ) + if supported_features & SUPPORT_ALARM_TRIGGER: + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "trigger", + } + ) + + return actions + + +async def async_call_action_from_config( + hass: HomeAssistant, config: dict, variables: dict, context: Context | None +) -> None: + """Execute a device action.""" + service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} + if CONF_CODE in config: + service_data[ATTR_CODE] = config[CONF_CODE] + + if config[CONF_TYPE] == "arm_away": + service = SERVICE_ALARM_ARM_AWAY + elif config[CONF_TYPE] == "arm_home": + service = SERVICE_ALARM_ARM_HOME + elif config[CONF_TYPE] == "arm_night": + service = SERVICE_ALARM_ARM_NIGHT + elif config[CONF_TYPE] == "disarm": + service = SERVICE_ALARM_DISARM + elif config[CONF_TYPE] == "trigger": + service = SERVICE_ALARM_TRIGGER + + await hass.services.async_call( + DOMAIN, service, service_data, blocking=True, context=context + ) + + +async def async_get_action_capabilities(hass, config): + """List action capabilities.""" + state = hass.states.get(config[CONF_ENTITY_ID]) + code_required = state.attributes.get(ATTR_CODE_ARM_REQUIRED) if state else False + + if config[CONF_TYPE] == "trigger" or ( + config[CONF_TYPE] != "disarm" and not code_required + ): + return {} + + return {"extra_fields": vol.Schema({vol.Optional(CONF_CODE): str})} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/device_condition.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/device_condition.py new file mode 100644 index 00000000000..3817cf37b45 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/device_condition.py @@ -0,0 +1,163 @@ +"""Provide the device automations for Alarm control panel.""" +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_CUSTOM_BYPASS, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_CONDITION, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_TYPE, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import condition, config_validation as cv, entity_registry +from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + +from . import DOMAIN +from .const import ( + CONDITION_ARMED_AWAY, + CONDITION_ARMED_CUSTOM_BYPASS, + CONDITION_ARMED_HOME, + CONDITION_ARMED_NIGHT, + CONDITION_DISARMED, + CONDITION_TRIGGERED, +) + +CONDITION_TYPES = { + CONDITION_TRIGGERED, + CONDITION_DISARMED, + CONDITION_ARMED_HOME, + CONDITION_ARMED_AWAY, + CONDITION_ARMED_NIGHT, + CONDITION_ARMED_CUSTOM_BYPASS, +} + +CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + } +) + + +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> list[dict[str, str]]: + """List device conditions for Alarm control panel devices.""" + registry = await entity_registry.async_get_registry(hass) + conditions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + state = hass.states.get(entry.entity_id) + + # We need a state or else we can't populate the different armed conditions + if state is None: + continue + + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + + # Add conditions for each entity that belongs to this integration + conditions += [ + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: CONDITION_DISARMED, + }, + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: CONDITION_TRIGGERED, + }, + ] + if supported_features & SUPPORT_ALARM_ARM_HOME: + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: CONDITION_ARMED_HOME, + } + ) + if supported_features & SUPPORT_ALARM_ARM_AWAY: + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: CONDITION_ARMED_AWAY, + } + ) + if supported_features & SUPPORT_ALARM_ARM_NIGHT: + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: CONDITION_ARMED_NIGHT, + } + ) + if supported_features & SUPPORT_ALARM_ARM_CUSTOM_BYPASS: + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: CONDITION_ARMED_CUSTOM_BYPASS, + } + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + if config[CONF_TYPE] == CONDITION_TRIGGERED: + state = STATE_ALARM_TRIGGERED + elif config[CONF_TYPE] == CONDITION_DISARMED: + state = STATE_ALARM_DISARMED + elif config[CONF_TYPE] == CONDITION_ARMED_HOME: + state = STATE_ALARM_ARMED_HOME + elif config[CONF_TYPE] == CONDITION_ARMED_AWAY: + state = STATE_ALARM_ARMED_AWAY + elif config[CONF_TYPE] == CONDITION_ARMED_NIGHT: + state = STATE_ALARM_ARMED_NIGHT + elif config[CONF_TYPE] == CONDITION_ARMED_CUSTOM_BYPASS: + state = STATE_ALARM_ARMED_CUSTOM_BYPASS + + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + return condition.state(hass, config[ATTR_ENTITY_ID], state) + + return test_is_state diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/device_trigger.py new file mode 100644 index 00000000000..b24716bb43e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/device_trigger.py @@ -0,0 +1,144 @@ +"""Provides device automations for Alarm control panel.""" +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.homeassistant.triggers import state as state_trigger +from homeassistant.const import ( + ATTR_SUPPORTED_FEATURES, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_FOR, + CONF_PLATFORM, + CONF_TYPE, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMING, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType + +from . import DOMAIN + +BASIC_TRIGGER_TYPES = {"triggered", "disarmed", "arming"} +TRIGGER_TYPES = BASIC_TRIGGER_TYPES | {"armed_home", "armed_away", "armed_night"} + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } +) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: + """List device triggers for Alarm control panel devices.""" + registry = await entity_registry.async_get_registry(hass) + triggers = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + entity_state = hass.states.get(entry.entity_id) + + # We need a state or else we can't populate the HVAC and preset modes. + if entity_state is None: + continue + + supported_features = entity_state.attributes[ATTR_SUPPORTED_FEATURES] + + # Add triggers for each entity that belongs to this integration + base_trigger = { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + + triggers += [ + { + **base_trigger, + CONF_TYPE: trigger, + } + for trigger in BASIC_TRIGGER_TYPES + ] + if supported_features & SUPPORT_ALARM_ARM_HOME: + triggers.append( + { + **base_trigger, + CONF_TYPE: "armed_home", + } + ) + if supported_features & SUPPORT_ALARM_ARM_AWAY: + triggers.append( + { + **base_trigger, + CONF_TYPE: "armed_away", + } + ) + if supported_features & SUPPORT_ALARM_ARM_NIGHT: + triggers.append( + { + **base_trigger, + CONF_TYPE: "armed_night", + } + ) + + return triggers + + +async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List trigger capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + if config[CONF_TYPE] == "triggered": + to_state = STATE_ALARM_TRIGGERED + elif config[CONF_TYPE] == "disarmed": + to_state = STATE_ALARM_DISARMED + elif config[CONF_TYPE] == "arming": + to_state = STATE_ALARM_ARMING + elif config[CONF_TYPE] == "armed_home": + to_state = STATE_ALARM_ARMED_HOME + elif config[CONF_TYPE] == "armed_away": + to_state = STATE_ALARM_ARMED_AWAY + elif config[CONF_TYPE] == "armed_night": + to_state = STATE_ALARM_ARMED_NIGHT + + state_config = { + state_trigger.CONF_PLATFORM: "state", + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state_trigger.CONF_TO: to_state, + } + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] + state_config = state_trigger.TRIGGER_SCHEMA(state_config) + return await state_trigger.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/group.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/group.py new file mode 100644 index 00000000000..4bfb1486814 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/group.py @@ -0,0 +1,30 @@ +"""Describe group states.""" + + +from homeassistant.components.group import GroupIntegrationRegistry +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_TRIGGERED, + STATE_OFF, +) +from homeassistant.core import HomeAssistant, callback + + +@callback +def async_describe_on_off_states( + hass: HomeAssistant, registry: GroupIntegrationRegistry +) -> None: + """Describe group on off states.""" + registry.on_off_states( + { + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_TRIGGERED, + }, + STATE_OFF, + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/manifest.json new file mode 100644 index 00000000000..e4cd0e27a39 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "alarm_control_panel", + "name": "Alarm Control Panel", + "documentation": "https://www.home-assistant.io/integrations/alarm_control_panel", + "codeowners": [], + "quality_scale": "internal" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/reproduce_state.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/reproduce_state.py new file mode 100644 index 00000000000..e7e4c07b8ad --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/reproduce_state.py @@ -0,0 +1,99 @@ +"""Reproduce an Alarm control panel state.""" +from __future__ import annotations + +import asyncio +from collections.abc import Iterable +import logging +from typing import Any + +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_TRIGGER, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) +from homeassistant.core import Context, HomeAssistant, State + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = { + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +} + + +async def _async_reproduce_state( + hass: HomeAssistant, + state: State, + *, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATE_ALARM_ARMED_AWAY: + service = SERVICE_ALARM_ARM_AWAY + elif state.state == STATE_ALARM_ARMED_CUSTOM_BYPASS: + service = SERVICE_ALARM_ARM_CUSTOM_BYPASS + elif state.state == STATE_ALARM_ARMED_HOME: + service = SERVICE_ALARM_ARM_HOME + elif state.state == STATE_ALARM_ARMED_NIGHT: + service = SERVICE_ALARM_ARM_NIGHT + elif state.state == STATE_ALARM_DISARMED: + service = SERVICE_ALARM_DISARM + elif state.state == STATE_ALARM_TRIGGERED: + service = SERVICE_ALARM_TRIGGER + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistant, + states: Iterable[State], + *, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, +) -> None: + """Reproduce Alarm control panel states.""" + await asyncio.gather( + *( + _async_reproduce_state( + hass, state, context=context, reproduce_options=reproduce_options + ) + for state in states + ) + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/services.yaml new file mode 100644 index 00000000000..8c148a6a1e0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/services.yaml @@ -0,0 +1,85 @@ +# Describes the format for available alarm control panel services + +alarm_disarm: + name: Disarm + description: Send the alarm the command for disarm. + target: + entity: + domain: alarm_control_panel + fields: + code: + name: Code + description: An optional code to disarm the alarm control panel with. + example: "1234" + selector: + text: + +alarm_arm_custom_bypass: + name: Arm with custom bypass + description: Send arm custom bypass command. + target: + entity: + domain: alarm_control_panel + fields: + code: + name: Code + description: An optional code to arm custom bypass the alarm control panel with. + example: "1234" + selector: + text: + +alarm_arm_home: + name: Arm home + description: Send the alarm the command for arm home. + target: + entity: + domain: alarm_control_panel + fields: + code: + name: Code + description: An optional code to arm home the alarm control panel with. + example: "1234" + selector: + text: + +alarm_arm_away: + name: Arm away + description: Send the alarm the command for arm away. + target: + entity: + domain: alarm_control_panel + fields: + code: + name: Code + description: An optional code to arm away the alarm control panel with. + example: "1234" + selector: + text: + +alarm_arm_night: + name: Arm night + description: Send the alarm the command for arm night. + target: + entity: + domain: alarm_control_panel + fields: + code: + name: Code + description: An optional code to arm night the alarm control panel with. + example: "1234" + selector: + text: + +alarm_trigger: + name: Trigger + description: Send the alarm the command for trigger. + target: + entity: + domain: alarm_control_panel + fields: + code: + name: Code + description: An optional code to trigger the alarm control panel with. + example: "1234" + selector: + text: diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/strings.json new file mode 100644 index 00000000000..de89d28082b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/strings.json @@ -0,0 +1,40 @@ +{ + "title": "Alarm control panel", + "device_automation": { + "action_type": { + "arm_away": "Arm {entity_name} away", + "arm_home": "Arm {entity_name} home", + "arm_night": "Arm {entity_name} night", + "disarm": "Disarm {entity_name}", + "trigger": "Trigger {entity_name}" + }, + "condition_type": { + "is_triggered": "{entity_name} is triggered", + "is_disarmed": "{entity_name} is disarmed", + "is_armed_home": "{entity_name} is armed home", + "is_armed_away": "{entity_name} is armed away", + "is_armed_night": "{entity_name} is armed night" + }, + "trigger_type": { + "triggered": "{entity_name} triggered", + "disarmed": "{entity_name} disarmed", + "armed_home": "{entity_name} armed home", + "armed_away": "{entity_name} armed away", + "armed_night": "{entity_name} armed night" + } + }, + "state": { + "_": { + "armed": "Armed", + "disarmed": "Disarmed", + "armed_home": "Armed home", + "armed_away": "Armed away", + "armed_night": "Armed night", + "armed_custom_bypass": "Armed custom bypass", + "pending": "Pending", + "arming": "Arming", + "disarming": "Disarming", + "triggered": "Triggered" + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/af.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/af.json new file mode 100644 index 00000000000..6f6a5c51c94 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/af.json @@ -0,0 +1,17 @@ +{ + "state": { + "_": { + "armed": "Gewapen", + "armed_away": "Gewapend weg", + "armed_custom_bypass": "Gewapende pasgemaakte omseil", + "armed_home": "Gewapend tuis", + "armed_night": "Gewapend nag", + "arming": "Bewapen Tans", + "disarmed": "Ontwapen", + "disarming": "Ontwapen Tans", + "pending": "Hangende", + "triggered": "Geaktiveer" + } + }, + "title": "Alarm beheer paneel" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ar.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ar.json new file mode 100644 index 00000000000..427b30eebbe --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ar.json @@ -0,0 +1,17 @@ +{ + "state": { + "_": { + "armed": "\u0645\u0633\u0644\u062d", + "armed_away": "\u0645\u0641\u0639\u0651\u0644 \u0641\u064a \u0627\u0644\u062e\u0627\u0631\u062c", + "armed_custom_bypass": "\u062a\u062c\u0627\u0648\u0632 \u0627\u0644\u062a\u0641\u0639\u064a\u0644", + "armed_home": "\u0645\u0641\u0639\u0651\u0644 \u0641\u064a \u0627\u0644\u0645\u0646\u0632\u0644", + "armed_night": "\u0645\u0641\u0639\u0651\u0644 \u0644\u064a\u0644", + "arming": "\u062c\u0627\u0631\u064a \u0627\u0644\u062a\u0641\u0639\u064a\u0644", + "disarmed": "\u063a\u064a\u0631 \u0645\u0641\u0639\u0651\u0644", + "disarming": "\u0625\u064a\u0642\u0627\u0641 \u0627\u0644\u0625\u0646\u0630\u0627\u0631", + "pending": "\u0642\u064a\u062f \u0627\u0644\u0625\u0646\u062a\u0638\u0627\u0631", + "triggered": "\u0645\u0641\u0639\u0651\u0644" + } + }, + "title": "\u0644\u0648\u062d\u0629 \u062a\u062d\u0643\u0645 \u0627\u0644\u0625\u0646\u0630\u0627\u0631" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/bg.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/bg.json new file mode 100644 index 00000000000..4eb04fa54fc --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/bg.json @@ -0,0 +1,33 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "\u0421\u043b\u043e\u0436\u0438 {entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 \u0432 \u0440\u0435\u0436\u0438\u043c \u043e\u0442\u0441\u044a\u0441\u0442\u0432\u0438\u0435", + "arm_home": "\u0421\u043b\u043e\u0436\u0438 {entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 \u0432 \u0440\u0435\u0436\u0438\u043c \u0432\u043a\u044a\u0449\u0438", + "arm_night": "\u0421\u043b\u043e\u0436\u0438 {entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 \u0432 \u043d\u043e\u0449\u0435\u043d \u0440\u0435\u0436\u0438\u043c", + "disarm": "\u0414\u0435\u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0439 {entity_name}", + "trigger": "\u0417\u0430\u0434\u0435\u0439\u0441\u0442\u0432\u0430\u043d\u0435 {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430", + "armed_home": "{entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 - \u0432\u043a\u044a\u0449\u0438", + "armed_night": "{entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 - \u043d\u043e\u0449", + "disarmed": "{entity_name} \u0434\u0435\u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0430", + "triggered": "{entity_name} \u0437\u0430\u0434\u0435\u0439\u0441\u0442\u0432\u0430\u043d\u0430" + } + }, + "state": { + "_": { + "armed": "\u041f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430", + "armed_away": "\u041f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430", + "armed_custom_bypass": "\u041f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430", + "armed_home": "\u041f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 - \u0432\u043a\u044a\u0449\u0438", + "armed_night": "\u041f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 - \u043d\u043e\u0449", + "arming": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435", + "disarmed": "\u0414\u0435\u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0430", + "disarming": "\u0414\u0435\u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435", + "pending": "\u0412 \u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0435", + "triggered": "\u0417\u0430\u0434\u0435\u0439\u0441\u0442\u0432\u0430\u043d" + } + }, + "title": "\u041a\u043e\u043d\u0442\u0440\u043e\u043b \u043d\u0430 \u0430\u043b\u0430\u0440\u043c\u0430" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/bs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/bs.json new file mode 100644 index 00000000000..00012852b52 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/bs.json @@ -0,0 +1,17 @@ +{ + "state": { + "_": { + "armed": "Aktiviran", + "armed_away": "Aktiviran izvan ku\u0107e", + "armed_custom_bypass": "Aktiviran pod specijalnim rezimom", + "armed_home": "Aktiviran kod ku\u0107e", + "armed_night": "Aktiviran no\u0107u", + "arming": "Aktivacija", + "disarmed": "Deaktiviran", + "disarming": "Deaktivacija", + "pending": "U is\u010dekivanju", + "triggered": "Pokrenut" + } + }, + "title": "Centralni sistem za alarm" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ca.json new file mode 100644 index 00000000000..dafef96b090 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ca.json @@ -0,0 +1,40 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Activa {entity_name} fora", + "arm_home": "Activa {entity_name} a casa", + "arm_night": "Activa {entity_name} nocturn", + "disarm": "Desactiva {entity_name}", + "trigger": "Dispara {entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name} est\u00e0 activada en mode 'a fora'", + "is_armed_home": "{entity_name} est\u00e0 activada en mode 'a casa'", + "is_armed_night": "{entity_name} est\u00e0 activada en mode 'nocturn'", + "is_disarmed": "{entity_name} est\u00e0 desactivada", + "is_triggered": "{entity_name} est\u00e0 disparada" + }, + "trigger_type": { + "armed_away": "{entity_name} activada en mode 'a fora'", + "armed_home": "{entity_name} activada en mode 'a casa'", + "armed_night": "{entity_name} activada en mode 'nocturn'", + "disarmed": "{entity_name} desactivada", + "triggered": "{entity_name} disparat/ada" + } + }, + "state": { + "_": { + "armed": "Activada", + "armed_away": "Activada, mode fora", + "armed_custom_bypass": "Activada, bypass personalitzat", + "armed_home": "Activada, mode a casa", + "armed_night": "Activada, mode nocturn", + "arming": "Activant", + "disarmed": "Desactivada", + "disarming": "Desactivant", + "pending": "Pendent", + "triggered": "Disparada" + } + }, + "title": "Panell de control d'alarma" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/cs.json new file mode 100644 index 00000000000..66786dfc0e2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/cs.json @@ -0,0 +1,40 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Aktivovat {entity_name} v re\u017eimu nep\u0159\u00edtomnost", + "arm_home": "Aktivovat {entity_name} v re\u017eimu domov", + "arm_night": "Aktivovat {entity_name} v no\u010dn\u00edm re\u017eimu", + "disarm": "Odbezpe\u010dit {entity_name}", + "trigger": "Spustit {entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name} je v re\u017eimu nep\u0159\u00edtomnost", + "is_armed_home": "{entity_name} je v re\u017eimu domov", + "is_armed_night": "{entity_name} je v no\u010dn\u00edm re\u017eimu", + "is_disarmed": "{entity_name} nen\u00ed zabezpe\u010den", + "is_triggered": "{entity_name} je spu\u0161t\u011bn" + }, + "trigger_type": { + "armed_away": "{entity_name} v re\u017eimu nep\u0159\u00edtomnost", + "armed_home": "{entity_name} v re\u017eimu domov", + "armed_night": "{entity_name} v no\u010dn\u00edm re\u017eimu", + "disarmed": "{entity_name} nezabezpe\u010den", + "triggered": "{entity_name} spu\u0161t\u011bn" + } + }, + "state": { + "_": { + "armed": "Zabezpe\u010deno", + "armed_away": "Nep\u0159\u00edtomnost", + "armed_custom_bypass": "Zabezpe\u010deno u\u017eivatelsk\u00fdm obejit\u00edm", + "armed_home": "Re\u017eim domov", + "armed_night": "No\u010dn\u00ed re\u017eim", + "arming": "Zabezpe\u010dov\u00e1n\u00ed", + "disarmed": "Nezabezpe\u010deno", + "disarming": "Odbezpe\u010dov\u00e1n\u00ed", + "pending": "\u010cekaj\u00edc\u00ed", + "triggered": "Spu\u0161t\u011bn" + } + }, + "title": "Ovl\u00e1dac\u00ed panel alarmu" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/cy.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/cy.json new file mode 100644 index 00000000000..a8a7e52af34 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/cy.json @@ -0,0 +1,17 @@ +{ + "state": { + "_": { + "armed": "Arfogi", + "armed_away": "Arfog i ffwrdd", + "armed_custom_bypass": "Ffordd osgoi larwm personol", + "armed_home": "Arfogi gartref", + "armed_night": "Arfog nos", + "arming": "Arfogi", + "disarmed": "Diarfogi", + "disarming": "Ddiarfogi", + "pending": "Yn yr arfaeth", + "triggered": "Sbarduno" + } + }, + "title": "Panel rheoli larwm" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/da.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/da.json new file mode 100644 index 00000000000..f3b04e26360 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/da.json @@ -0,0 +1,33 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Tilkobl {entity_name} ude", + "arm_home": "Tilkobl {entity_name} hjemme", + "arm_night": "Tilkobl {entity_name} nat", + "disarm": "Frakobl {entity_name}", + "trigger": "Udl\u00f8s {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} tilkoblet ude", + "armed_home": "{entity_name} tilkoblet hjemme", + "armed_night": "{entity_name} tilkoblet nat", + "disarmed": "{entity_name} frakoblet", + "triggered": "{entity_name} udl\u00f8st" + } + }, + "state": { + "_": { + "armed": "Tilkoblet", + "armed_away": "Tilkoblet ude", + "armed_custom_bypass": "Tilkoblet brugerdefineret bypass", + "armed_home": "Tilkoblet hjemme", + "armed_night": "Tilkoblet nat", + "arming": "Tilkobler", + "disarmed": "Frakoblet", + "disarming": "Frakobler", + "pending": "Afventer", + "triggered": "Udl\u00f8st" + } + }, + "title": "Alarmkontrolpanel" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/de.json new file mode 100644 index 00000000000..a671c388932 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/de.json @@ -0,0 +1,40 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Aktiviere {entity_name} Unterwegs", + "arm_home": "Aktiviere {entity_name} Zuhause", + "arm_night": "Aktiviere {entity_name} Nacht-Modus", + "disarm": "Deaktivere {entity_name}", + "trigger": "Ausl\u00f6ser {entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name} ist aktiviert - Unterwegs", + "is_armed_home": "{entity_name} ist aktiviert - Zuhause", + "is_armed_night": "{entity_name} ist aktiviert - Nacht", + "is_disarmed": "{entity_name} ist deaktiviert", + "is_triggered": "{entity_name} wurde ausgel\u00f6st" + }, + "trigger_type": { + "armed_away": "{entity_name} Unterwegs", + "armed_home": "{entity_name} Zuhause", + "armed_night": "{entity_name} Nacht-Modus", + "disarmed": "{entity_name} deaktiviert", + "triggered": "{entity_name} ausgel\u00f6st" + } + }, + "state": { + "_": { + "armed": "Aktiv", + "armed_away": "Aktiv, abwesend", + "armed_custom_bypass": "Aktiv, benutzerdefiniert", + "armed_home": "Aktiv, zu Hause", + "armed_night": "Aktiv, Nacht", + "arming": "Aktiviere", + "disarmed": "Inaktiv", + "disarming": "Deaktiviere", + "pending": "Ausstehend", + "triggered": "Ausgel\u00f6st" + } + }, + "title": "Alarmanlage" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/el.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/el.json new file mode 100644 index 00000000000..5b37be59d47 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/el.json @@ -0,0 +1,17 @@ +{ + "state": { + "_": { + "armed": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2", + "armed_away": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03bc\u03b1\u03ba\u03c1\u03b9\u03ac", + "armed_custom_bypass": "\u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03b7 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03b5\u03bd\u03b5\u03c1\u03b3\u03ae", + "armed_home": "\u03a3\u03c0\u03af\u03c4\u03b9 \u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf", + "armed_night": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03b2\u03c1\u03ac\u03b4\u03c5", + "arming": "\u038c\u03c0\u03bb\u03b9\u03c3\u03b7", + "disarmed": "\u0391\u03c6\u03bf\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2", + "disarming": "\u0391\u03c6\u03cc\u03c0\u03bb\u03b9\u03c3\u03b7", + "pending": "\u0395\u03ba\u03ba\u03c1\u03b5\u03bc\u03ae\u03c2", + "triggered": "\u03a0\u03b1\u03c1\u03b1\u03b2\u03af\u03b1\u03c3\u03b7" + } + }, + "title": "\u03a0\u03af\u03bd\u03b1\u03ba\u03b1\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c9\u03bd" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/en.json new file mode 100644 index 00000000000..b364d850461 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/en.json @@ -0,0 +1,40 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Arm {entity_name} away", + "arm_home": "Arm {entity_name} home", + "arm_night": "Arm {entity_name} night", + "disarm": "Disarm {entity_name}", + "trigger": "Trigger {entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name} is armed away", + "is_armed_home": "{entity_name} is armed home", + "is_armed_night": "{entity_name} is armed night", + "is_disarmed": "{entity_name} is disarmed", + "is_triggered": "{entity_name} is triggered" + }, + "trigger_type": { + "armed_away": "{entity_name} armed away", + "armed_home": "{entity_name} armed home", + "armed_night": "{entity_name} armed night", + "disarmed": "{entity_name} disarmed", + "triggered": "{entity_name} triggered" + } + }, + "state": { + "_": { + "armed": "Armed", + "armed_away": "Armed away", + "armed_custom_bypass": "Armed custom bypass", + "armed_home": "Armed home", + "armed_night": "Armed night", + "arming": "Arming", + "disarmed": "Disarmed", + "disarming": "Disarming", + "pending": "Pending", + "triggered": "Triggered" + } + }, + "title": "Alarm control panel" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/es-419.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/es-419.json new file mode 100644 index 00000000000..7de15a91608 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/es-419.json @@ -0,0 +1,40 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Habilitar {entity_name} fuera de casa", + "arm_home": "Habilitar {entity_name} en casa", + "arm_night": "Habilitar {entity_name} de noche", + "disarm": "Deshabilitar {entity_name}", + "trigger": "Activar {entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name} est\u00e1 habilitada fuera de casa", + "is_armed_home": "{entity_name} est\u00e1 habilitada en casa", + "is_armed_night": "{entity_name} est\u00e1 habilitada de noche", + "is_disarmed": "{entity_name} est\u00e1 deshabilitada", + "is_triggered": "{entity_name} est\u00e1 activada" + }, + "trigger_type": { + "armed_away": "{entity_name} habilitada fuera de casa", + "armed_home": "{entity_name} habilitada en casa", + "armed_night": "{entity_name} habilitada de noche", + "disarmed": "{entity_name} deshabilitada", + "triggered": "{entity_name} activada" + } + }, + "state": { + "_": { + "armed": "Armado", + "armed_away": "Armado Fuera de Casa", + "armed_custom_bypass": "Armada zona espec\u00edfica", + "armed_home": "Armado en Casa", + "armed_night": "Armado Nocturno", + "arming": "Armando", + "disarmed": "Desarmado", + "disarming": "Desarmando", + "pending": "Pendiente", + "triggered": "Activado" + } + }, + "title": "Panel de control de alarma" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/es.json new file mode 100644 index 00000000000..ab4e4a20cce --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/es.json @@ -0,0 +1,40 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Armar {entity_name} exterior", + "arm_home": "Armar {entity_name} modo casa", + "arm_night": "Armar {entity_name} por la noche", + "disarm": "Desarmar {entity_name}", + "trigger": "Lanzar {entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name} est\u00e1 armada ausente", + "is_armed_home": "{entity_name} est\u00e1 armada en casa", + "is_armed_night": "{entity_name} est\u00e1 armada noche", + "is_disarmed": "{entity_name} est\u00e1 desarmada", + "is_triggered": "{entity_name} est\u00e1 disparada" + }, + "trigger_type": { + "armed_away": "{entity_name} armada ausente", + "armed_home": "{entity_name} armada en casa", + "armed_night": "{entity_name} armada noche", + "disarmed": "{entity_name} desarmada", + "triggered": "{entity_name} activado" + } + }, + "state": { + "_": { + "armed": "Armada", + "armed_away": "Armada ausente", + "armed_custom_bypass": "Armada personalizada", + "armed_home": "Armada en casa", + "armed_night": "Armada noche", + "arming": "Armando", + "disarmed": "Desarmada", + "disarming": "Desarmando", + "pending": "Pendiente", + "triggered": "Disparada" + } + }, + "title": "Panel de control de alarmas" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/et.json new file mode 100644 index 00000000000..cc4bb6f1ea3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/et.json @@ -0,0 +1,40 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Valvesta {entity_name}", + "arm_home": "Valvesta {entity_name} kodus re\u017eiimis", + "arm_night": "Valvesta {entity_name} \u00f6\u00f6re\u017eiimis", + "disarm": "V\u00f5ta {entity_name} valvest maha", + "trigger": "K\u00e4ivita {entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name} on valvestatud", + "is_armed_home": "{entity_name} on valvestatud kodure\u017eiimis", + "is_armed_night": "{entity_name} on valvestatud \u00f6\u00f6re\u017eiimis", + "is_disarmed": "{entity_name} on valve alt maas", + "is_triggered": "{entity_name} on h\u00e4iret andnud" + }, + "trigger_type": { + "armed_away": "{entity_name} valvestati", + "armed_home": "{entity_name} valvestati kodure\u017eiimis", + "armed_night": "{entity_name} valvestati \u00f6\u00f6re\u017eiimis", + "disarmed": "{entity_name} v\u00f5eti valvest maha", + "triggered": "{entity_name} andis h\u00e4iret" + } + }, + "state": { + "_": { + "armed": "Valves", + "armed_away": "Valves eemal", + "armed_custom_bypass": "Valves, eranditega", + "armed_home": "Valves kodus", + "armed_night": "Valves \u00f6ine", + "arming": "Valvestab", + "disarmed": "Maas", + "disarming": "Maas...", + "pending": "Ootel", + "triggered": "H\u00e4ires" + } + }, + "title": "Valvekeskuse juhtpaneel" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/eu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/eu.json new file mode 100644 index 00000000000..e483eeac44d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/eu.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "pending": "Zain", + "triggered": "Abiarazita" + } + }, + "title": "Alarmen kontrol panela" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/fa.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/fa.json new file mode 100644 index 00000000000..1aa489f7d93 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/fa.json @@ -0,0 +1,17 @@ +{ + "state": { + "_": { + "armed": "\u0645\u0635\u0644\u062d \u0634\u062f\u0647", + "armed_away": "\u0645\u0633\u0644\u062d \u0634\u062f\u0647 \u0628\u06cc\u0631\u0648\u0646", + "armed_custom_bypass": "\u0628\u0627\u06cc\u06af\u0627\u0646\u06cc \u0633\u0641\u0627\u0631\u0634\u06cc \u0645\u0633\u0644\u062d", + "armed_home": "\u0645\u0633\u0644\u062d \u0634\u062f\u0647 \u062e\u0627\u0646\u0647", + "armed_night": "\u0645\u0633\u0644\u062d \u0634\u062f\u0647 \u0634\u0628", + "arming": "\u062f\u0631 \u062d\u0627\u0644 \u0645\u0633\u0644\u062d \u06a9\u0631\u062f\u0646", + "disarmed": "\u063a\u06cc\u0631 \u0645\u0633\u0644\u062d", + "disarming": "\u062f\u0631 \u062d\u0627\u0644 \u063a\u06cc\u0631 \u0645\u0633\u0644\u062d \u06a9\u0631\u062f\u0646", + "pending": "\u062f\u0631 \u0627\u0646\u062a\u0638\u0627\u0631", + "triggered": "\u0631\u0627\u0647 \u0627\u0646\u062f\u0627\u062e\u062a\u0647 \u0634\u062f\u0647" + } + }, + "title": "\u06a9\u0646\u062a\u0631\u0644 \u067e\u0646\u0644 \u0622\u0644\u0627\u0631\u0645" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/fi.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/fi.json new file mode 100644 index 00000000000..1a77c621458 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/fi.json @@ -0,0 +1,17 @@ +{ + "state": { + "_": { + "armed": "Viritetty", + "armed_away": "Viritetty (poissa)", + "armed_custom_bypass": "Virityksen ohittaminen", + "armed_home": "Viritetty (kotona)", + "armed_night": "Viritetty (y\u00f6)", + "arming": "Viritys", + "disarmed": "Viritys pois", + "disarming": "Virityksen poisto", + "pending": "Odottaa", + "triggered": "Lauennut" + } + }, + "title": "H\u00e4lytysasetukset" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/fr.json new file mode 100644 index 00000000000..c7e010e805e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/fr.json @@ -0,0 +1,40 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Armer {entity_name} en mode \"sortie\"", + "arm_home": "Armer {entity_name} en mode \"maison\"", + "arm_night": "Armer {entity_name} en mode \"nuit\"", + "disarm": "D\u00e9sarmer {entity_name}", + "trigger": "D\u00e9clencheur {entity_name}" + }, + "condition_type": { + "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_disarmed": "{entity_name} est d\u00e9sarm\u00e9", + "is_triggered": "{entity_name} est d\u00e9clench\u00e9" + }, + "trigger_type": { + "armed_away": "Armer {entity_name} en mode \"sortie\"", + "armed_home": "Armer {entity_name} en mode \"maison\"", + "armed_night": "Armer {entity_name} en mode \"nuit\"", + "disarmed": "{entity_name} d\u00e9sarm\u00e9", + "triggered": "{entity_name} d\u00e9clench\u00e9" + } + }, + "state": { + "_": { + "armed": "Activ\u00e9", + "armed_away": "Enclench\u00e9e (absent)", + "armed_custom_bypass": "Arm\u00e9 avec exception personnalis\u00e9e", + "armed_home": "Enclench\u00e9e (pr\u00e9sent)", + "armed_night": "Enclench\u00e9 (nuit)", + "arming": "Activation", + "disarmed": "D\u00e9sactiv\u00e9e", + "disarming": "D\u00e9sactivation", + "pending": "En attente", + "triggered": "D\u00e9clench\u00e9" + } + }, + "title": "Panneau de contr\u00f4le d'alarme" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/gsw.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/gsw.json new file mode 100644 index 00000000000..615ad7dc950 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/gsw.json @@ -0,0 +1,15 @@ +{ + "state": { + "_": { + "armed": "Scharf", + "armed_away": "Scharf usswerts", + "armed_home": "Scharf dihei", + "armed_night": "Scharf Nacht", + "arming": "Scharf stel\u00e4", + "disarmed": "Nid scharf", + "disarming": "Entsperr\u00e4", + "pending": "Usstehehnd", + "triggered": "Usgl\u00f6sst" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/he.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/he.json new file mode 100644 index 00000000000..544b23f5629 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/he.json @@ -0,0 +1,17 @@ +{ + "state": { + "_": { + "armed": "\u05d3\u05e8\u05d5\u05da", + "armed_away": "\u05d3\u05e8\u05d5\u05da \u05dc\u05d0 \u05d1\u05d1\u05d9\u05ea", + "armed_custom_bypass": "\u05de\u05e2\u05e7\u05e3 \u05de\u05d5\u05ea\u05d0\u05dd \u05d0\u05d9\u05e9\u05d9\u05ea \u05d3\u05e8\u05d5\u05da", + "armed_home": "\u05d4\u05d1\u05d9\u05ea \u05d3\u05e8\u05d5\u05da", + "armed_night": "\u05d3\u05e8\u05d5\u05da \u05dc\u05d9\u05dc\u05d4", + "arming": "\u05de\u05e4\u05e2\u05d9\u05dc", + "disarmed": "\u05de\u05e0\u05d5\u05d8\u05e8\u05dc", + "disarming": "\u05de\u05e0\u05d8\u05e8\u05dc", + "pending": "\u05de\u05de\u05ea\u05d9\u05df", + "triggered": "\u05d4\u05d5\u05e4\u05e2\u05dc" + } + }, + "title": "\u05dc\u05d5\u05d7 \u05d1\u05e7\u05e8\u05d4 \u05e9\u05dc \u05d0\u05d6\u05e2\u05e7\u05d4" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/hr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/hr.json new file mode 100644 index 00000000000..57308c14e30 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/hr.json @@ -0,0 +1,17 @@ +{ + "state": { + "_": { + "armed": "Aktiviran", + "armed_away": "Aktiviran odsutno", + "armed_custom_bypass": "Aktiviran", + "armed_home": "Aktiviran doma", + "armed_night": "Aktiviran no\u010dni", + "arming": "Aktiviranje", + "disarmed": "Deaktiviran", + "disarming": "Deaktiviranje", + "pending": "U tijeku", + "triggered": "Okinut" + } + }, + "title": "Upravlja\u010dka plo\u010da za alarm" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/hu.json new file mode 100644 index 00000000000..81fa10311ef --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/hu.json @@ -0,0 +1,33 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "{entity_name} \u00e9les\u00edt\u00e9se t\u00e1voz\u00f3 m\u00f3dban", + "arm_home": "{entity_name} \u00e9les\u00edt\u00e9se otthon marad\u00f3 m\u00f3dban", + "arm_night": "{entity_name} \u00e9les\u00edt\u00e9se \u00e9jszakai m\u00f3dban", + "disarm": "{entity_name} hat\u00e1stalan\u00edt\u00e1sa", + "trigger": "{entity_name} riaszt\u00e1si esem\u00e9ny ind\u00edt\u00e1sa" + }, + "trigger_type": { + "armed_away": "{entity_name} t\u00e1voz\u00f3 m\u00f3dban lett \u00e9les\u00edtve", + "armed_home": "{entity_name} otthon marad\u00f3 m\u00f3dban lett \u00e9les\u00edtve", + "armed_night": "{entity_name} \u00e9jszakai m\u00f3dban lett \u00e9les\u00edtve", + "disarmed": "{entity_name} hat\u00e1stalan\u00edtva lett", + "triggered": "{entity_name} riaszt\u00e1sba ker\u00fclt" + } + }, + "state": { + "_": { + "armed": "\u00c9les\u00edtve", + "armed_away": "\u00c9les\u00edtve t\u00e1vol", + "armed_custom_bypass": "\u00c9les\u00edtve \u00e1thidal\u00e1ssal", + "armed_home": "\u00c9les\u00edtve otthon", + "armed_night": "\u00c9les\u00edtve \u00e9jszaka", + "arming": "\u00c9les\u00edt\u00e9s", + "disarmed": "Hat\u00e1stalan\u00edtva", + "disarming": "Hat\u00e1stalan\u00edt\u00e1s", + "pending": "F\u00fcgg\u0151ben", + "triggered": "Riaszt\u00e1s" + } + }, + "title": "Riaszt\u00f3 k\u00f6zpont" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/hy.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/hy.json new file mode 100644 index 00000000000..58788b33577 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/hy.json @@ -0,0 +1,17 @@ +{ + "state": { + "_": { + "armed": "\u0536\u056b\u0576\u057e\u0561\u056e", + "armed_away": "\u0536\u056b\u0576\u057e\u0561\u056e", + "armed_custom_bypass": "\u0536\u056b\u0576\u0574\u0561\u0576 \u0561\u0576\u0570\u0561\u057f\u0561\u056f\u0561\u0576 \u056f\u0578\u0564", + "armed_home": "\u0536\u056b\u0576\u057e\u0561\u056e \u057f\u0578\u0582\u0576", + "armed_night": "\u0536\u056b\u0576\u057e\u0561\u056e \u0563\u056b\u0577\u0565\u0580", + "arming": "\u0536\u056b\u0576\u0565\u056c", + "disarmed": "\u0536\u056b\u0576\u0561\u0569\u0561\u0583\u057e\u0561\u056e", + "disarming": "\u0536\u056b\u0576\u0561\u0569\u0561\u0583\u0578\u0572", + "pending": "\u054d\u057a\u0561\u057d\u0578\u0582\u0574", + "triggered": "\u057a\u0561\u057f\u0573\u0561\u057c\u0568" + } + }, + "title": "\u054f\u0561\u0563\u0576\u0561\u057a\u056b \u056f\u0561\u057c\u0561\u057e\u0561\u0580\u0574\u0561\u0576 \u057e\u0561\u0570\u0561\u0576\u0561\u056f" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/id.json new file mode 100644 index 00000000000..f1676ce8c75 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/id.json @@ -0,0 +1,40 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Aktifkan {entity_name} untuk keluar", + "arm_home": "Aktifkan {entity_name} untuk di rumah", + "arm_night": "Aktifkan {entity_name} untuk malam", + "disarm": "Nonaktifkan {entity_name}", + "trigger": "Picu {entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name} diaktifkan untuk keluar", + "is_armed_home": "{entity_name} diaktifkan untuk di rumah", + "is_armed_night": "{entity_name} diaktifkan untuk malam", + "is_disarmed": "{entity_name} dinonaktifkan", + "is_triggered": "{entity_name} dipicu" + }, + "trigger_type": { + "armed_away": "{entity_name} diaktifkan untuk keluar", + "armed_home": "{entity_name} diaktifkan untuk di rumah", + "armed_night": "{entity_name} diaktifkan untuk malam", + "disarmed": "{entity_name} dinonaktifkan", + "triggered": "{entity_name} dipicu" + } + }, + "state": { + "_": { + "armed": "Diaktifkan", + "armed_away": "Diaktifkan untuk keluar", + "armed_custom_bypass": "Diaktifkan khusus", + "armed_home": "Diaktifkan untuk di rumah", + "armed_night": "Diaktifkan untuk malam", + "arming": "Mengaktifkan", + "disarmed": "Dinonaktifkan", + "disarming": "Dinonaktifkan", + "pending": "Tertunda", + "triggered": "Terpicu" + } + }, + "title": "Kontrol panel alarm" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/is.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/is.json new file mode 100644 index 00000000000..eda11e6177f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/is.json @@ -0,0 +1,16 @@ +{ + "state": { + "_": { + "armed": "\u00c1 ver\u00f0i", + "armed_away": "\u00c1 ver\u00f0i \u00fati", + "armed_home": "\u00c1 ver\u00f0i heima", + "armed_night": "\u00c1 ver\u00f0i n\u00f3tt", + "arming": "Set \u00e1 v\u00f6r\u00f0", + "disarmed": "ekki \u00e1 ver\u00f0i", + "disarming": "tek af ver\u00f0i", + "pending": "B\u00ed\u00f0ur", + "triggered": "R\u00e6st" + } + }, + "title": "Stj\u00f3rnbor\u00f0 \u00f6ryggiskerfis" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/it.json new file mode 100644 index 00000000000..1574f88541b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/it.json @@ -0,0 +1,40 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Armare {entity_name} uscito", + "arm_home": "Armare {entity_name} casa", + "arm_night": "Armare {entity_name} notte", + "disarm": "Disarmare {entity_name}", + "trigger": "Attivazione {entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name} \u00e8 attivo in modalit\u00e0 fuori casa", + "is_armed_home": "{entity_name} \u00e8 attivo in modalit\u00e0 a casa", + "is_armed_night": "{entity_name} \u00e8 attivo in modalit\u00e0 notte", + "is_disarmed": "{entity_name} \u00e8 disattivo", + "is_triggered": "{entity_name} \u00e8 attivato" + }, + "trigger_type": { + "armed_away": "{entity_name} attivato in modalit\u00e0 fuori casa", + "armed_home": "{entity_name} attivato in modalit\u00e0 a casa", + "armed_night": "{entity_name} attivato in modalit\u00e0 notte", + "disarmed": "{entity_name} disattivato", + "triggered": "{entity_name} attivato" + } + }, + "state": { + "_": { + "armed": "Attivo", + "armed_away": "Attivo fuori casa", + "armed_custom_bypass": "Attivo con bypass personalizzato", + "armed_home": "Attivo in casa", + "armed_night": "Attivo Notte", + "arming": "In Attivazione", + "disarmed": "Disattivo", + "disarming": "In Disattivazione", + "pending": "In sospeso", + "triggered": "Attivato" + } + }, + "title": "Pannello di Controllo degli Allarmi" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ja.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ja.json new file mode 100644 index 00000000000..3eceb75b597 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ja.json @@ -0,0 +1,7 @@ +{ + "state": { + "_": { + "triggered": "\u30c8\u30ea\u30ac\u30fc" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ko.json new file mode 100644 index 00000000000..0fd766ba0b9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ko.json @@ -0,0 +1,40 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "{entity_name}\uc744(\ub97c) \uc678\ucd9c\uacbd\ube44\ub85c \uc124\uc815\ud558\uae30", + "arm_home": "{entity_name}\uc744(\ub97c) \uc7ac\uc2e4\uacbd\ube44\ub85c \uc124\uc815\ud558\uae30", + "arm_night": "{entity_name}\uc744(\ub97c) \uc57c\uac04\uacbd\ube44\ub85c \uc124\uc815\ud558\uae30", + "disarm": "{entity_name}\uc744(\ub97c) \uacbd\ube44\ud574\uc81c\ub85c \uc124\uc815\ud558\uae30", + "trigger": "{entity_name}\uc744(\ub97c) \ud2b8\ub9ac\uac70\ud558\uae30" + }, + "condition_type": { + "is_armed_away": "{entity_name}\uc774(\uac00) \uc678\ucd9c \uacbd\ube44\ubaa8\ub4dc \uc0c1\ud0dc\uc774\uba74", + "is_armed_home": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uacbd\ube44\ubaa8\ub4dc \uc0c1\ud0dc\uc774\uba74", + "is_armed_night": "{entity_name}\uc774(\uac00) \uc57c\uac04 \uacbd\ube44\ubaa8\ub4dc \uc0c1\ud0dc\uc774\uba74", + "is_disarmed": "{entity_name}\uc774(\uac00) \ud574\uc81c \uc0c1\ud0dc\uc774\uba74", + "is_triggered": "{entity_name}\uc774(\uac00) \ud2b8\ub9ac\uac70 \ub418\uc5c8\uc73c\uba74" + }, + "trigger_type": { + "armed_away": "{entity_name}\uc774(\uac00) \uc678\ucd9c \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5c8\uc744 \ub54c", + "armed_home": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5c8\uc744 \ub54c", + "armed_night": "{entity_name}\uc774(\uac00) \uc57c\uac04 \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5c8\uc744 \ub54c", + "disarmed": "{entity_name}\uc774(\uac00) \ud574\uc81c\ub418\uc5c8\uc744 \ub54c", + "triggered": "{entity_name}\uc774(\uac00) \ud2b8\ub9ac\uac70\ub418\uc5c8\uc744 \ub54c" + } + }, + "state": { + "_": { + "armed": "\uacbd\ube44 \uc911", + "armed_away": "\uacbd\ube44 \uc911(\uc678\ucd9c)", + "armed_custom_bypass": "\uacbd\ube44 \uc911 (\uc0ac\uc6a9\uc790 \uc6b0\ud68c)", + "armed_home": "\uacbd\ube44 \uc911(\uc7ac\uc2e4)", + "armed_night": "\uacbd\ube44 \uc911(\uc57c\uac04)", + "arming": "\uacbd\ube44 \uc911", + "disarmed": "\ud574\uc81c\ub428", + "disarming": "\ud574\uc81c\uc911", + "pending": "\ubcf4\ub958\uc911", + "triggered": "\uc791\ub3d9\ub428" + } + }, + "title": "\uc54c\ub78c\uc81c\uc5b4\ud310" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/lb.json new file mode 100644 index 00000000000..5a441693726 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/lb.json @@ -0,0 +1,40 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "{entity_name} fir \u00ebnnerwee uschalten", + "arm_home": "{entity_name} fir doheem uschalten", + "arm_night": "{entity_name} fir Nuecht uschalten", + "disarm": "{entity_name} entsch\u00e4rfen", + "trigger": "{entity_name} ausl\u00e9isen" + }, + "condition_type": { + "is_armed_away": "{entity_name} ass ugeschalt fir Ennerwee", + "is_armed_home": "{entity_name} ass ugeschalt fir Doheem", + "is_armed_night": "{entity_name} ass ugeschalt fir Nuecht", + "is_disarmed": "{entity_name} ass entsch\u00e4rft", + "is_triggered": "{entity_name} ass ausgel\u00e9ist" + }, + "trigger_type": { + "armed_away": "{entity_name} ugeschalt fir Ennerwee", + "armed_home": "{entity_name} ugeschalt fir Doheem", + "armed_night": "{entity_name} ugeschalt fir Nuecht", + "disarmed": "{entity_name} entsch\u00e4rft", + "triggered": "{entity_name} ausgel\u00e9ist" + } + }, + "state": { + "_": { + "armed": "Aktiv\u00e9iert", + "armed_away": "Aktiv\u00e9iert \u00cbnnerwee", + "armed_custom_bypass": "Aktiv, Benotzerdefin\u00e9iert", + "armed_home": "Aktiv\u00e9iert Doheem", + "armed_night": "Aktiv\u00e9iert Nuecht", + "arming": "Aktiv\u00e9ieren", + "disarmed": "Desaktiv\u00e9iert", + "disarming": "Desaktiv\u00e9ieren", + "pending": "Ustoend", + "triggered": "Ausgel\u00e9ist" + } + }, + "title": "Kontroll Feld Alarm" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/lt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/lt.json new file mode 100644 index 00000000000..c8a44246004 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/lt.json @@ -0,0 +1,13 @@ +{ + "state": { + "_": { + "armed": "U\u017erakinta", + "armed_home": "Nam\u0173 apsauga \u012fjungta", + "arming": "Saugojimo re\u017eimo \u012fjungimas", + "disarmed": "Atrakinta", + "disarming": "Saugojimo re\u017eimo i\u0161jungimas", + "pending": "Laukiama", + "triggered": "Aktyvinta" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/lv.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/lv.json new file mode 100644 index 00000000000..e77f05f4812 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/lv.json @@ -0,0 +1,17 @@ +{ + "state": { + "_": { + "armed": "Piesl\u0113gta", + "armed_away": "Piesl\u0113gta uz promb\u016btni", + "armed_custom_bypass": "Piesl\u0113gts piel\u0101gots apvedce\u013c\u0161", + "armed_home": "Piesl\u0113gta m\u0101j\u0101s", + "armed_night": "Piesl\u0113gta uz nakti", + "arming": "Piesl\u0113dzas", + "disarmed": "Atsl\u0113gta", + "disarming": "Atsl\u0113dzas", + "pending": "Gaida", + "triggered": "Aktiviz\u0113ta" + } + }, + "title": "Signaliz\u0101cijas vad\u012bbas panelis" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/nb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/nb.json new file mode 100644 index 00000000000..ec2e8b92e1e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/nb.json @@ -0,0 +1,17 @@ +{ + "state": { + "_": { + "armed": "Armert", + "armed_away": "Armert borte", + "armed_custom_bypass": "Armert tilpasset unntak", + "armed_home": "Armert hjemme", + "armed_night": "Armert natt", + "arming": "Armerer", + "disarmed": "Avsl\u00e5tt", + "disarming": "Skrur av", + "pending": "Venter", + "triggered": "Utl\u00f8st" + } + }, + "title": "Alarm kontrollpanel" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/nl.json new file mode 100644 index 00000000000..0a0f33d6181 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/nl.json @@ -0,0 +1,40 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Inschakelen {entity_name} afwezig", + "arm_home": "Inschakelen {entity_name} thuis", + "arm_night": "Inschakelen {entity_name} nacht", + "disarm": "Uitschakelen {entity_name}", + "trigger": "Trigger {entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name} afwezig ingeschakeld", + "is_armed_home": "{entity_name} thuis ingeschakeld", + "is_armed_night": "{entity_name} nachtstand ingeschakeld", + "is_disarmed": "{entity_name} is uitgeschakeld", + "is_triggered": "{entity_name} wordt geactiveerd" + }, + "trigger_type": { + "armed_away": "{entity_name} afwezig ingeschakeld", + "armed_home": "{entity_name} thuis ingeschakeld", + "armed_night": "{entity_name} nachtstand ingeschakeld", + "disarmed": "{entity_name} uitgeschakeld", + "triggered": "{entity_name} geactiveerd" + } + }, + "state": { + "_": { + "armed": "Ingeschakeld", + "armed_away": "Ingeschakeld afwezig", + "armed_custom_bypass": "Ingeschakeld met overbrugging(en)", + "armed_home": "Ingeschakeld thuis", + "armed_night": "Ingeschakeld nacht", + "arming": "Schakelt in", + "disarmed": "Uitgeschakeld", + "disarming": "Schakelt uit", + "pending": "In wacht", + "triggered": "Geactiveerd" + } + }, + "title": "Alarm bedieningspaneel" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/nn.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/nn.json new file mode 100644 index 00000000000..f8932a995b9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/nn.json @@ -0,0 +1,17 @@ +{ + "state": { + "_": { + "armed": "P\u00e5sl\u00e5tt", + "armed_away": "P\u00e5 for borte", + "armed_custom_bypass": "Armert tilpassa unntak", + "armed_home": "P\u00e5 for heime", + "armed_night": "P\u00e5 for natta", + "arming": "Skrur p\u00e5", + "disarmed": "Avsl\u00e5tt", + "disarming": "Skrur av", + "pending": "I vente av", + "triggered": "Utl\u00f8yst" + } + }, + "title": "Alarmkontrollpanel" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/no.json new file mode 100644 index 00000000000..465dd250086 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/no.json @@ -0,0 +1,40 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Aktiver {entity_name} borte", + "arm_home": "Aktiver {entity_name} hjemme", + "arm_night": "Aktiver {entity_name} natt", + "disarm": "Deaktiver {entity_name}", + "trigger": "Utl\u00f8ser {entity_name}" + }, + "condition_type": { + "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_disarmed": "{entity_name} er deaktivert", + "is_triggered": "{entity_name} er utl\u00f8st" + }, + "trigger_type": { + "armed_away": "{entity_name} aktivert borte", + "armed_home": "{entity_name} aktivert hjemme", + "armed_night": "{entity_name} aktivert natt", + "disarmed": "{entity_name} deaktivert", + "triggered": "{entity_name} utl\u00f8st" + } + }, + "state": { + "_": { + "armed": "Armert", + "armed_away": "Armert borte", + "armed_custom_bypass": "Armert tilpasset unntak", + "armed_home": "Armert hjemme", + "armed_night": "Armert natt", + "arming": "Armerer", + "disarmed": "Avsl\u00e5tt", + "disarming": "Disarmer", + "pending": "Ventende", + "triggered": "Utl\u00f8st" + } + }, + "title": "Alarm kontrollpanel" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/pl.json new file mode 100644 index 00000000000..0fd3045d1df --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/pl.json @@ -0,0 +1,40 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "uzbr\u00f3j (poza domem) {entity_name}", + "arm_home": "uzbr\u00f3j (w domu) {entity_name}", + "arm_night": "uzbr\u00f3j (noc) {entity_name}", + "disarm": "rozbr\u00f3j {entity_name}", + "trigger": "wyzw\u00f3l {entity_name}" + }, + "condition_type": { + "is_armed_away": "alarm {entity_name} jest uzbrojony (poza domem)", + "is_armed_home": "alarm {entity_name} jest uzbrojony (w domu)", + "is_armed_night": "alarm {entity_name} jest uzbrojony (noc)", + "is_disarmed": "alarm {entity_name} jest rozbrojony", + "is_triggered": "alarm {entity_name} jest wyzwolony" + }, + "trigger_type": { + "armed_away": "alarm {entity_name} zostanie uzbrojony (poza domem)", + "armed_home": "alarm {entity_name} zostanie uzbrojony (w domu)", + "armed_night": "alarm {entity_name} zostanie uzbrojony (noc)", + "disarmed": "alarm {entity_name} zostanie rozbrojony", + "triggered": "alarm {entity_name} zostanie wyzwolony" + } + }, + "state": { + "_": { + "armed": "uzbrojony", + "armed_away": "uzbrojony (poza domem)", + "armed_custom_bypass": "uzbrojony (cz\u0119\u015bciowo)", + "armed_home": "uzbrojony (w domu)", + "armed_night": "uzbrojony (noc)", + "arming": "uzbrajanie", + "disarmed": "rozbrojony", + "disarming": "rozbrajanie", + "pending": "oczekuje", + "triggered": "wyzwolony" + } + }, + "title": "Panel kontrolny alarmu" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/pt-BR.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/pt-BR.json new file mode 100644 index 00000000000..07e005cba03 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/pt-BR.json @@ -0,0 +1,40 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Armar {entity_name} longe", + "arm_home": "Armar {entity_name} casa", + "arm_night": "Armar {entity_name} noite", + "disarm": "Desarmar {entity_name}", + "trigger": "Disparar {entidade_nome}" + }, + "condition_type": { + "is_armed_away": "{entity_name} est\u00e1 armado modo longe", + "is_armed_home": "{entity_name} est\u00e1 armadado modo casa", + "is_armed_night": "{entity_name} est\u00e1 armadado modo noite", + "is_disarmed": "{entity_name} est\u00e1 desarmado", + "is_triggered": "{entity_name} est\u00e1 acionado" + }, + "trigger_type": { + "armed_away": "{entity_name} armado modo longe", + "armed_home": "{entity_name} armadado modo casa", + "armed_night": "{entity_name} armadado para noite", + "disarmed": "{entity_name} desarmado", + "triggered": "{entity_name} acionado" + } + }, + "state": { + "_": { + "armed": "Armado", + "armed_away": "Armado ausente", + "armed_custom_bypass": "Armado em \u00e1reas espec\u00edficas", + "armed_home": "Armado casa", + "armed_night": "Armado noite", + "arming": "Armando", + "disarmed": "Desarmado", + "disarming": "Desarmando", + "pending": "Pendente", + "triggered": "Acionado" + } + }, + "title": "Painel de controle do alarme" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/pt.json new file mode 100644 index 00000000000..e4293b81731 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/pt.json @@ -0,0 +1,24 @@ +{ + "device_automation": { + "action_type": { + "arm_home": "Armar casa {entity_name}", + "arm_night": "Armar noite {entity_name}", + "disarm": "Desarmar {entity_name}" + } + }, + "state": { + "_": { + "armed": "Armado", + "armed_away": "Armado ausente", + "armed_custom_bypass": "Armado com desvio personalizado", + "armed_home": "Armado Casa", + "armed_night": "Armado noite", + "arming": "A armar", + "disarmed": "Desarmado", + "disarming": "A desarmar", + "pending": "Pendente", + "triggered": "Despoletado" + } + }, + "title": "Painel de controlo do alarme" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ro.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ro.json new file mode 100644 index 00000000000..57af2d045d3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ro.json @@ -0,0 +1,17 @@ +{ + "state": { + "_": { + "armed": "Armat", + "armed_away": "Armat plecat", + "armed_custom_bypass": "Armare personalizat\u0103", + "armed_home": "Armat acas\u0103", + "armed_night": "Armat noaptea", + "arming": "Armare", + "disarmed": "Dezarmat", + "disarming": "Dezarmare", + "pending": "\u00cen a\u0219teptare", + "triggered": "Declan\u0219at" + } + }, + "title": "Panoul de control alarma" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ru.json new file mode 100644 index 00000000000..f390f017328 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ru.json @@ -0,0 +1,40 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u0435 \u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "arm_home": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u0414\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "arm_night": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u043e\u0447\u044c\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "disarm": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u0445\u0440\u0430\u043d\u0443 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "trigger": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442" + }, + "condition_type": { + "is_armed_away": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u0435 \u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "is_armed_home": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u0414\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "is_armed_night": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u043e\u0447\u044c\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "is_disarmed": "\u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0430 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "is_triggered": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442" + }, + "trigger_type": { + "armed_away": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u0435 \u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "armed_home": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u0414\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "armed_night": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u043e\u0447\u044c\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "disarmed": "\u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0430 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "triggered": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442" + } + }, + "state": { + "_": { + "armed": "\u041f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u043e\u0439", + "armed_away": "\u041e\u0445\u0440\u0430\u043d\u0430 (\u043d\u0435 \u0434\u043e\u043c\u0430)", + "armed_custom_bypass": "\u041e\u0445\u0440\u0430\u043d\u0430 \u0441 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f\u043c\u0438", + "armed_home": "\u041e\u0445\u0440\u0430\u043d\u0430 (\u0434\u043e\u043c\u0430)", + "armed_night": "\u041e\u0445\u0440\u0430\u043d\u0430 (\u043d\u043e\u0447\u044c)", + "arming": "\u041f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0443", + "disarmed": "\u0421\u043d\u044f\u0442\u043e \u0441 \u043e\u0445\u0440\u0430\u043d\u044b", + "disarming": "\u0421\u043d\u044f\u0442\u0438\u0435 \u0441 \u043e\u0445\u0440\u0430\u043d\u044b", + "pending": "\u041f\u0435\u0440\u0435\u0445\u043e\u0434 \u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0443", + "triggered": "\u0421\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u043d\u0438\u0435" + } + }, + "title": "\u041f\u0430\u043d\u0435\u043b\u044c \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/sk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/sk.json new file mode 100644 index 00000000000..ceff70c00a6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/sk.json @@ -0,0 +1,17 @@ +{ + "state": { + "_": { + "armed": "Akt\u00edvny", + "armed_away": "Akt\u00edvny v nepr\u00edtomnosti", + "armed_custom_bypass": "Zak\u00f3dovan\u00e9 prisp\u00f4soben\u00e9 vyl\u00fa\u010denie", + "armed_home": "Akt\u00edvny doma", + "armed_night": "Akt\u00edvny v noci", + "arming": "Aktivuje sa", + "disarmed": "Neakt\u00edvny", + "disarming": "Deaktivuje sa", + "pending": "\u010cak\u00e1 sa", + "triggered": "Spusten\u00fd" + } + }, + "title": "Ovl\u00e1dac\u00ed panel alarmu" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/sl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/sl.json new file mode 100644 index 00000000000..6ccef2cead6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/sl.json @@ -0,0 +1,40 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Vklju\u010di {entity_name} zdoma", + "arm_home": "Vklju\u010di {entity_name} doma", + "arm_night": "Vklju\u010di {entity_name} no\u010d", + "disarm": "Razoro\u017ei {entity_name}", + "trigger": "Spro\u017ei {entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name} je oboro\u017een na \"zdoma\"", + "is_armed_home": "{entity_name} je oboro\u017een na \"dom\"", + "is_armed_night": "{entity_name} je oboro\u017een na \"no\u010d\"", + "is_disarmed": "{entity_name} razoro\u017een", + "is_triggered": "{entity_name} spro\u017een" + }, + "trigger_type": { + "armed_away": "{entity_name} oboro\u017een - zdoma", + "armed_home": "{entity_name} oboro\u017een - dom", + "armed_night": "{entity_name} oboro\u017een - no\u010d", + "disarmed": "{entity_name} razoro\u017een", + "triggered": "{entity_name} spro\u017een" + } + }, + "state": { + "_": { + "armed": "Omogo\u010den", + "armed_away": "Omogo\u010den-zunaj", + "armed_custom_bypass": "Vklopljen izjeme po meri", + "armed_home": "Omogo\u010den-doma", + "armed_night": "Omogo\u010den-no\u010d", + "arming": "Omogo\u010danje", + "disarmed": "Onemogo\u010den", + "disarming": "Onemogo\u010danje", + "pending": "V teku", + "triggered": "Spro\u017een" + } + }, + "title": "Nadzorna plo\u0161\u010da Alarma" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/sv.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/sv.json new file mode 100644 index 00000000000..1f375eb5f1d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/sv.json @@ -0,0 +1,33 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Larma {entity_name} borta", + "arm_home": "Larma {entity_name} hemma", + "arm_night": "Larma {entity_name} natt", + "disarm": "Avlarma {entity_name}", + "trigger": "Utl\u00f6sare {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} larmad borta", + "armed_home": "{entity_name} larmad hemma", + "armed_night": "{entity_name} larmad natt", + "disarmed": "{entity_name} bortkopplad", + "triggered": "{entity_name} utl\u00f6st" + } + }, + "state": { + "_": { + "armed": "Larmat", + "armed_away": "Larmat", + "armed_custom_bypass": "Larm f\u00f6rbikopplat", + "armed_home": "Hemmalarmat", + "armed_night": "Nattlarmat", + "arming": "Tillkopplar", + "disarmed": "Avlarmat", + "disarming": "Fr\u00e5nkopplar", + "pending": "V\u00e4ntande", + "triggered": "Utl\u00f6st" + } + }, + "title": "Larmkontrollpanel" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ta.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ta.json new file mode 100644 index 00000000000..731c9815d92 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/ta.json @@ -0,0 +1,16 @@ +{ + "state": { + "_": { + "armed": "\u0b8e\u0b9a\u0bcd\u0b9a\u0bb0\u0bbf\u0b95\u0bcd\u0b95\u0bc8 \u0b92\u0bb2\u0bbf \u0b85\u0bae\u0bc8\u0b95\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0b9f\u0bcd\u0b9f\u0bc1\u0bb3\u0bcd\u0bb3\u0ba4\u0bc1", + "armed_away": "\u0b8e\u0b9a\u0bcd\u0b9a\u0bb0\u0bbf\u0b95\u0bcd\u0b95\u0bc8 \u0b92\u0bb2\u0bbf \u0bb5\u0bc6\u0bb3\u0bbf\u0baf\u0bc7", + "armed_custom_bypass": "\u0bb5\u0bbf\u0bb0\u0bc1\u0baa\u0bcd\u0baa \u0b8e\u0b9a\u0bcd\u0b9a\u0bb0\u0bbf\u0b95\u0bcd\u0b95\u0bc8 \u0b92\u0bb2\u0bbf", + "armed_home": "\u0b8e\u0b9a\u0bcd\u0b9a\u0bb0\u0bbf\u0b95\u0bcd\u0b95\u0bc8 \u0b92\u0bb2\u0bbf \u0bae\u0bc1\u0b95\u0baa\u0bcd\u0baa\u0bc1", + "armed_night": "\u0b8e\u0b9a\u0bcd\u0b9a\u0bb0\u0bbf\u0b95\u0bcd\u0b95\u0bc8 \u0b92\u0bb2\u0bbf \u0b87\u0bb0\u0bb5\u0bbf\u0bb2\u0bcd", + "arming": "\u0b8e\u0b9a\u0bcd\u0b9a\u0bb0\u0bbf\u0b95\u0bcd\u0b95\u0bc8 \u0b92\u0bb2\u0bbf \u0b85\u0bae\u0bc8\u0b95\u0bcd\u0b95\u0bbf\u0bb1\u0ba4\u0bc1", + "disarmed": "\u0b8e\u0b9a\u0bcd\u0b9a\u0bb0\u0bbf\u0b95\u0bcd\u0b95\u0bc8 \u0b92\u0bb2\u0bbf \u0b85\u0bae\u0bc8\u0b95\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0b9f\u0bb5\u0bbf\u0bb2\u0bcd\u0bb2\u0bc8", + "disarming": "\u0b8e\u0b9a\u0bcd\u0b9a\u0bb0\u0bbf\u0b95\u0bcd\u0b95\u0bc8 \u0b92\u0bb2\u0bbf \u0ba8\u0bc0\u0b95\u0bcd\u0b95\u0bae\u0bcd", + "pending": "\u0ba8\u0bbf\u0bb2\u0bc1\u0bb5\u0bc8\u0baf\u0bbf\u0bb2\u0bcd", + "triggered": "\u0ba4\u0bc2\u0ba3\u0bcd\u0b9f\u0baa\u0bcd\u0baa\u0b9f\u0bc1\u0b95\u0bbf\u0bb1\u0ba4\u0bc1" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/te.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/te.json new file mode 100644 index 00000000000..dd5357238e3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/te.json @@ -0,0 +1,17 @@ +{ + "state": { + "_": { + "armed": "\u0c2d\u0c26\u0c4d\u0c30\u0c24 \u0c35\u0c41\u0c02\u0c26\u0c3f", + "armed_away": "\u0c07\u0c02\u0c1f \u0c2c\u0c2f\u0c1f \u0c2d\u0c26\u0c4d\u0c30\u0c24", + "armed_custom_bypass": "\u0c2d\u0c26\u0c4d\u0c30\u0c24 \u0c15\u0c38\u0c4d\u0c1f\u0c2e\u0c4d \u0c2c\u0c48\u0c2a\u0c3e\u0c38\u0c4d", + "armed_home": "\u0c38\u0c46\u0c15\u0c4d\u0c2f\u0c42\u0c30\u0c3f\u0c1f\u0c40 \u0c38\u0c3f\u0c38\u0c4d\u0c1f\u0c2e\u0c4d \u0c06\u0c28\u0c4d \u0c1a\u0c47\u0c2f\u0c2c\u0c21\u0c3f\u0c02\u0c26\u0c3f", + "armed_night": "\u0c30\u0c3e\u0c24\u0c4d\u0c30\u0c3f \u0c2a\u0c42\u0c1f \u0c2d\u0c26\u0c4d\u0c30\u0c24", + "arming": "\u0c2d\u0c26\u0c4d\u0c30\u0c3f\u0c02\u0c1a\u0c41\u0c1f", + "disarmed": "\u0c2d\u0c26\u0c4d\u0c30\u0c24 \u0c32\u0c47\u0c26\u0c41", + "disarming": "\u0c2d\u0c26\u0c4d\u0c30\u0c24 \u0c24\u0c40\u0c38\u0c3f\u0c35\u0c47\u0c2f\u0c41\u0c1f", + "pending": "\u0c2a\u0c46\u0c02\u0c21\u0c3f\u0c02\u0c17\u0c4d", + "triggered": "\u0c0a\u0c2a\u0c02\u0c26\u0c41\u0c15\u0c41\u0c02\u0c26\u0c3f" + } + }, + "title": "\u0c05\u0c32\u0c3e\u0c30\u0c02 \u0c28\u0c3f\u0c2f\u0c02\u0c24\u0c4d\u0c30\u0c23 \u0c2a\u0c4d\u0c2f\u0c3e\u0c28\u0c46\u0c32\u0c4d" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/th.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/th.json new file mode 100644 index 00000000000..ada983bba16 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/th.json @@ -0,0 +1,17 @@ +{ + "state": { + "_": { + "armed": "\u0e40\u0e1b\u0e34\u0e14\u0e01\u0e32\u0e23\u0e1b\u0e49\u0e2d\u0e07\u0e01\u0e31\u0e19", + "armed_away": "\u0e40\u0e1b\u0e34\u0e14\u0e01\u0e32\u0e23\u0e1b\u0e49\u0e2d\u0e07\u0e01\u0e31\u0e19-\u0e42\u0e2b\u0e21\u0e14\u0e44\u0e21\u0e48\u0e2d\u0e22\u0e39\u0e48\u0e1a\u0e49\u0e32\u0e19", + "armed_custom_bypass": "\u0e1b\u0e49\u0e2d\u0e07\u0e01\u0e31\u0e19\u0e42\u0e14\u0e22\u0e01\u0e33\u0e2b\u0e19\u0e14\u0e40\u0e2d\u0e07", + "armed_home": "\u0e40\u0e1b\u0e34\u0e14\u0e01\u0e32\u0e23\u0e1b\u0e49\u0e2d\u0e07\u0e01\u0e31\u0e19-\u0e42\u0e2b\u0e21\u0e14\u0e2d\u0e22\u0e39\u0e48\u0e1a\u0e49\u0e32\u0e19", + "armed_night": "\u0e40\u0e1b\u0e34\u0e14\u0e01\u0e32\u0e23\u0e1b\u0e49\u0e2d\u0e07\u0e01\u0e31\u0e19-\u0e42\u0e2b\u0e21\u0e14\u0e01\u0e25\u0e32\u0e07\u0e04\u0e37\u0e19", + "arming": "\u0e40\u0e1b\u0e34\u0e14\u0e01\u0e32\u0e23\u0e1b\u0e49\u0e2d\u0e07\u0e01\u0e31\u0e19", + "disarmed": "\u0e1b\u0e25\u0e14\u0e01\u0e32\u0e23\u0e1b\u0e49\u0e2d\u0e07\u0e01\u0e31\u0e19", + "disarming": "\u0e1b\u0e25\u0e14\u0e01\u0e32\u0e23\u0e1b\u0e49\u0e2d\u0e07\u0e01\u0e31\u0e19", + "pending": "\u0e04\u0e49\u0e32\u0e07\u0e2d\u0e22\u0e39\u0e48", + "triggered": "\u0e16\u0e39\u0e01\u0e01\u0e23\u0e30\u0e15\u0e38\u0e49\u0e19" + } + }, + "title": "\u0e41\u0e1c\u0e07\u0e04\u0e27\u0e1a\u0e04\u0e38\u0e21\u0e2a\u0e31\u0e0d\u0e0d\u0e32\u0e13\u0e40\u0e15\u0e37\u0e2d\u0e19\u0e20\u0e31\u0e22" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/tr.json new file mode 100644 index 00000000000..cc509430436 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/tr.json @@ -0,0 +1,23 @@ +{ + "device_automation": { + "trigger_type": { + "disarmed": "{entity_name} b\u0131rak\u0131ld\u0131", + "triggered": "{entity_name} tetiklendi" + } + }, + "state": { + "_": { + "armed": "Etkin", + "armed_away": "Etkin d\u0131\u015far\u0131da", + "armed_custom_bypass": "Alarm etkin \u00f6zel baypas", + "armed_home": "Etkin evde", + "armed_night": "Etkin gece", + "arming": "Alarm etkinle\u015fiyor", + "disarmed": "Etkisiz", + "disarming": "Alarm devre d\u0131\u015f\u0131", + "pending": "Beklemede", + "triggered": "Tetiklendi" + } + }, + "title": "Alarm kontrol paneli" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/uk.json new file mode 100644 index 00000000000..b50fd9f459d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/uk.json @@ -0,0 +1,40 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0435 \u0432\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "arm_home": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u0412\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "arm_night": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0456\u0447\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "disarm": "\u0412\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043e\u0445\u043e\u0440\u043e\u043d\u0443 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "trigger": "{entity_name} \u0441\u043f\u0440\u0430\u0446\u044c\u043e\u0432\u0443\u0454" + }, + "condition_type": { + "is_armed_away": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0435 \u0432\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "is_armed_home": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u0412\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "is_armed_night": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0456\u0447\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "is_disarmed": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0430 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "is_triggered": "{entity_name} \u0441\u043f\u0440\u0430\u0446\u044c\u043e\u0432\u0443\u0454" + }, + "trigger_type": { + "armed_away": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0435 \u0432\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "armed_home": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u0412\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "armed_night": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0456\u0447\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "disarmed": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0430 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "triggered": "{entity_name} \u0441\u043f\u0440\u0430\u0446\u044c\u043e\u0432\u0443\u0454" + } + }, + "state": { + "_": { + "armed": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430", + "armed_away": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u041d\u0435 \u0432\u0434\u043e\u043c\u0430)", + "armed_custom_bypass": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 \u0437 \u0432\u0438\u043d\u044f\u0442\u043a\u0430\u043c\u0438", + "armed_home": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u0412\u0434\u043e\u043c\u0430)", + "armed_night": "\u041d\u0456\u0447\u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0430", + "arming": "\u0421\u0442\u0430\u0432\u043b\u044e \u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0443", + "disarmed": "\u0417\u043d\u044f\u0442\u043e \u0437 \u043e\u0445\u043e\u0440\u043e\u043d\u0438", + "disarming": "\u0417\u043d\u044f\u0442\u0442\u044f", + "pending": "\u041e\u0447\u0456\u043a\u0443\u044e", + "triggered": "\u0422\u0440\u0438\u0432\u043e\u0433\u0430" + } + }, + "title": "\u041f\u0430\u043d\u0435\u043b\u044c \u043a\u0435\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u0441\u0438\u0433\u043d\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u0454\u044e" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/vi.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/vi.json new file mode 100644 index 00000000000..3a0fb34950b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/vi.json @@ -0,0 +1,17 @@ +{ + "state": { + "_": { + "armed": "K\u00edch ho\u1ea1t an ninh", + "armed_away": "B\u1ea3o v\u1ec7 \u0111i v\u1eafng", + "armed_custom_bypass": "T\u00f9y ch\u1ec9nh b\u1ecf qua An ninh", + "armed_home": "B\u1ea3o v\u1ec7 \u1edf nh\u00e0", + "armed_night": "Ban \u0111\u00eam", + "arming": "K\u00edch ho\u1ea1t", + "disarmed": "V\u00f4 hi\u1ec7u h\u00f3a", + "disarming": "Gi\u1ea3i gi\u00e1p", + "pending": "\u0110ang ch\u1edd x\u1eed l\u00fd", + "triggered": "K\u00edch ho\u1ea1t" + } + }, + "title": "B\u1ea3ng \u0111i\u1ec1u khi\u1ec3n an ninh" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/zh-Hans.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/zh-Hans.json new file mode 100644 index 00000000000..fa819e71b49 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/zh-Hans.json @@ -0,0 +1,40 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "{entity_name} \u79bb\u5bb6\u8b66\u6212", + "arm_home": "{entity_name} \u5728\u5bb6\u8b66\u6212", + "arm_night": "{entity_name} \u591c\u95f4\u8b66\u6212", + "disarm": "\u89e3\u9664 {entity_name} \u8b66\u6212", + "trigger": "\u89e6\u53d1 {entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name} \u79bb\u5bb6\u8b66\u6212", + "is_armed_home": "{entity_name} \u5728\u5bb6\u8b66\u6212", + "is_armed_night": "{entity_name} \u591c\u95f4\u8b66\u6212", + "is_disarmed": "{entity_name} \u8b66\u6212\u5df2\u89e3\u9664", + "is_triggered": "{entity_name} \u8b66\u62a5\u5df2\u89e6\u53d1" + }, + "trigger_type": { + "armed_away": "{entity_name} \u79bb\u5bb6\u8b66\u6212", + "armed_home": "{entity_name} \u5728\u5bb6\u8b66\u6212", + "armed_night": "{entity_name} \u591c\u95f4\u8b66\u6212", + "disarmed": "{entity_name} \u8b66\u6212\u89e3\u9664", + "triggered": "{entity_name} \u89e6\u53d1\u8b66\u62a5" + } + }, + "state": { + "_": { + "armed": "\u8b66\u6212", + "armed_away": "\u79bb\u5bb6\u8b66\u6212", + "armed_custom_bypass": "\u81ea\u5b9a\u4e49\u533a\u57df\u8b66\u6212", + "armed_home": "\u5728\u5bb6\u8b66\u6212", + "armed_night": "\u591c\u95f4\u8b66\u6212", + "arming": "\u8b66\u6212\u4e2d", + "disarmed": "\u8b66\u6212\u89e3\u9664", + "disarming": "\u8b66\u6212\u89e3\u9664", + "pending": "\u6302\u8d77", + "triggered": "\u5df2\u89e6\u53d1" + } + }, + "title": "\u8b66\u62a5\u63a7\u5236\u5668" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/zh-Hant.json new file mode 100644 index 00000000000..2dac00f9990 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarm_control_panel/translations/zh-Hant.json @@ -0,0 +1,40 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "\u8a2d\u5b9a{entity_name}\u5916\u51fa\u6a21\u5f0f", + "arm_home": "\u8a2d\u5b9a{entity_name}\u8fd4\u5bb6\u6a21\u5f0f", + "arm_night": "\u8a2d\u5b9a{entity_name}\u591c\u9593\u6a21\u5f0f", + "disarm": "\u89e3\u9664{entity_name}", + "trigger": "\u89f8\u767c{entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name}\u8a2d\u5b9a\u5916\u51fa", + "is_armed_home": "{entity_name}\u8a2d\u5b9a\u5728\u5bb6", + "is_armed_night": "{entity_name}\u8a2d\u5b9a\u591c\u9593", + "is_disarmed": "{entity_name}\u5df2\u89e3\u9664", + "is_triggered": "{entity_name}\u5df2\u89f8\u767c" + }, + "trigger_type": { + "armed_away": "{entity_name}\u8a2d\u5b9a\u5916\u51fa", + "armed_home": "{entity_name}\u8a2d\u5b9a\u5728\u5bb6", + "armed_night": "{entity_name}\u8a2d\u5b9a\u591c\u9593", + "disarmed": "{entity_name}\u5df2\u89e3\u9664", + "triggered": "{entity_name}\u5df2\u89f8\u767c" + } + }, + "state": { + "_": { + "armed": "\u5df2\u8b66\u6212", + "armed_away": "\u96e2\u5bb6\u8b66\u6212", + "armed_custom_bypass": "\u8b66\u6212\u6a21\u5f0f\u72c0\u614b", + "armed_home": "\u5728\u5bb6\u8b66\u6212", + "armed_night": "\u591c\u9593\u8b66\u6212", + "arming": "\u8b66\u6212\u4e2d", + "disarmed": "\u8b66\u6212\u89e3\u9664", + "disarming": "\u89e3\u9664\u4e2d", + "pending": "\u7b49\u5f85\u4e2d", + "triggered": "\u5df2\u89f8\u767c" + } + }, + "title": "\u8b66\u6212\u63a7\u5236\u9762\u677f" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/__init__.py new file mode 100644 index 00000000000..aff7dd8c5ba --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/__init__.py @@ -0,0 +1,156 @@ +"""Support for AlarmDecoder devices.""" +from datetime import timedelta +import logging + +from adext import AdExt +from alarmdecoder.devices import SerialDevice, SocketDevice +from alarmdecoder.util import NoDeviceError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + CONF_PROTOCOL, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util + +from .const import ( + CONF_DEVICE_BAUD, + CONF_DEVICE_PATH, + DATA_AD, + DATA_REMOVE_STOP_LISTENER, + DATA_REMOVE_UPDATE_LISTENER, + DATA_RESTART, + DOMAIN, + PROTOCOL_SERIAL, + PROTOCOL_SOCKET, + SIGNAL_PANEL_MESSAGE, + SIGNAL_REL_MESSAGE, + SIGNAL_RFX_MESSAGE, + SIGNAL_ZONE_FAULT, + SIGNAL_ZONE_RESTORE, +) + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["alarm_control_panel", "sensor", "binary_sensor"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up AlarmDecoder config flow.""" + undo_listener = entry.add_update_listener(_update_listener) + + ad_connection = entry.data + protocol = ad_connection[CONF_PROTOCOL] + + def stop_alarmdecoder(event): + """Handle the shutdown of AlarmDecoder.""" + if not hass.data.get(DOMAIN): + return + _LOGGER.debug("Shutting down alarmdecoder") + hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = False + controller.close() + + async def open_connection(now=None): + """Open a connection to AlarmDecoder.""" + try: + await hass.async_add_executor_job(controller.open, baud) + except NoDeviceError: + _LOGGER.debug("Failed to connect. Retrying in 5 seconds") + hass.helpers.event.async_track_point_in_time( + open_connection, dt_util.utcnow() + timedelta(seconds=5) + ) + return + _LOGGER.debug("Established a connection with the alarmdecoder") + hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = True + + def handle_closed_connection(event): + """Restart after unexpected loss of connection.""" + if not hass.data[DOMAIN][entry.entry_id][DATA_RESTART]: + return + hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = False + _LOGGER.warning("AlarmDecoder unexpectedly lost connection") + hass.add_job(open_connection) + + def handle_message(sender, message): + """Handle message from AlarmDecoder.""" + hass.helpers.dispatcher.dispatcher_send(SIGNAL_PANEL_MESSAGE, message) + + def handle_rfx_message(sender, message): + """Handle RFX message from AlarmDecoder.""" + hass.helpers.dispatcher.dispatcher_send(SIGNAL_RFX_MESSAGE, message) + + def zone_fault_callback(sender, zone): + """Handle zone fault from AlarmDecoder.""" + hass.helpers.dispatcher.dispatcher_send(SIGNAL_ZONE_FAULT, zone) + + def zone_restore_callback(sender, zone): + """Handle zone restore from AlarmDecoder.""" + hass.helpers.dispatcher.dispatcher_send(SIGNAL_ZONE_RESTORE, zone) + + def handle_rel_message(sender, message): + """Handle relay or zone expander message from AlarmDecoder.""" + hass.helpers.dispatcher.dispatcher_send(SIGNAL_REL_MESSAGE, message) + + baud = ad_connection.get(CONF_DEVICE_BAUD) + if protocol == PROTOCOL_SOCKET: + host = ad_connection[CONF_HOST] + port = ad_connection[CONF_PORT] + controller = AdExt(SocketDevice(interface=(host, port))) + if protocol == PROTOCOL_SERIAL: + path = ad_connection[CONF_DEVICE_PATH] + controller = AdExt(SerialDevice(interface=path)) + + controller.on_message += handle_message + controller.on_rfx_message += handle_rfx_message + controller.on_zone_fault += zone_fault_callback + controller.on_zone_restore += zone_restore_callback + controller.on_close += handle_closed_connection + controller.on_expander_message += handle_rel_message + + remove_stop_listener = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder + ) + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + DATA_AD: controller, + DATA_REMOVE_UPDATE_LISTENER: undo_listener, + DATA_REMOVE_STOP_LISTENER: remove_stop_listener, + DATA_RESTART: False, + } + + await open_connection() + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a AlarmDecoder entry.""" + hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = False + + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + if not unload_ok: + return False + + hass.data[DOMAIN][entry.entry_id][DATA_REMOVE_UPDATE_LISTENER]() + hass.data[DOMAIN][entry.entry_id][DATA_REMOVE_STOP_LISTENER]() + await hass.async_add_executor_job(hass.data[DOMAIN][entry.entry_id][DATA_AD].close) + + if hass.data[DOMAIN][entry.entry_id]: + hass.data[DOMAIN].pop(entry.entry_id) + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) + + return True + + +async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Handle options update.""" + _LOGGER.debug("AlarmDecoder options updated: %s", entry.as_dict()["options"]) + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/alarm_control_panel.py new file mode 100644 index 00000000000..47da48de66f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -0,0 +1,219 @@ +"""Support for AlarmDecoder-based alarm control panels (Honeywell/DSC).""" +import voluptuous as vol + +from homeassistant.components.alarm_control_panel import ( + FORMAT_NUMBER, + AlarmControlPanelEntity, +) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_CODE, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_platform +import homeassistant.helpers.config_validation as cv + +from .const import ( + CONF_ALT_NIGHT_MODE, + CONF_AUTO_BYPASS, + CONF_CODE_ARM_REQUIRED, + DATA_AD, + DEFAULT_ARM_OPTIONS, + DOMAIN, + OPTIONS_ARM, + SIGNAL_PANEL_MESSAGE, +) + +SERVICE_ALARM_TOGGLE_CHIME = "alarm_toggle_chime" + +SERVICE_ALARM_KEYPRESS = "alarm_keypress" +ATTR_KEYPRESS = "keypress" + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): + """Set up for AlarmDecoder alarm panels.""" + options = entry.options + arm_options = options.get(OPTIONS_ARM, DEFAULT_ARM_OPTIONS) + client = hass.data[DOMAIN][entry.entry_id][DATA_AD] + + entity = AlarmDecoderAlarmPanel( + client=client, + auto_bypass=arm_options[CONF_AUTO_BYPASS], + code_arm_required=arm_options[CONF_CODE_ARM_REQUIRED], + alt_night_mode=arm_options[CONF_ALT_NIGHT_MODE], + ) + async_add_entities([entity]) + + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_ALARM_TOGGLE_CHIME, + { + vol.Required(ATTR_CODE): cv.string, + }, + "alarm_toggle_chime", + ) + + platform.async_register_entity_service( + SERVICE_ALARM_KEYPRESS, + { + vol.Required(ATTR_KEYPRESS): cv.string, + }, + "alarm_keypress", + ) + + +class AlarmDecoderAlarmPanel(AlarmControlPanelEntity): + """Representation of an AlarmDecoder-based alarm panel.""" + + def __init__(self, client, auto_bypass, code_arm_required, alt_night_mode): + """Initialize the alarm panel.""" + self._client = client + self._display = "" + self._name = "Alarm Panel" + self._state = None + self._ac_power = None + self._alarm_event_occurred = None + self._backlight_on = None + self._battery_low = None + self._check_zone = None + self._chime = None + self._entry_delay_off = None + self._programming_mode = None + self._ready = None + self._zone_bypassed = None + self._auto_bypass = auto_bypass + self._code_arm_required = code_arm_required + self._alt_night_mode = alt_night_mode + + async def async_added_to_hass(self): + """Register callbacks.""" + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_PANEL_MESSAGE, self._message_callback + ) + ) + + def _message_callback(self, message): + """Handle received messages.""" + if message.alarm_sounding or message.fire_alarm: + self._state = STATE_ALARM_TRIGGERED + elif message.armed_away: + self._state = STATE_ALARM_ARMED_AWAY + elif message.armed_home and (message.entry_delay_off or message.perimeter_only): + self._state = STATE_ALARM_ARMED_NIGHT + elif message.armed_home: + self._state = STATE_ALARM_ARMED_HOME + else: + self._state = STATE_ALARM_DISARMED + + self._ac_power = message.ac_power + self._alarm_event_occurred = message.alarm_event_occurred + self._backlight_on = message.backlight_on + self._battery_low = message.battery_low + self._check_zone = message.check_zone + self._chime = message.chime_on + self._entry_delay_off = message.entry_delay_off + self._programming_mode = message.programming_mode + self._ready = message.ready + self._zone_bypassed = message.zone_bypassed + + self.schedule_update_ha_state() + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def code_format(self): + """Return one or more digits/characters.""" + return FORMAT_NUMBER + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + + @property + def code_arm_required(self): + """Whether the code is required for arm actions.""" + return self._code_arm_required + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return { + "ac_power": self._ac_power, + "alarm_event_occurred": self._alarm_event_occurred, + "backlight_on": self._backlight_on, + "battery_low": self._battery_low, + "check_zone": self._check_zone, + "chime": self._chime, + "entry_delay_off": self._entry_delay_off, + "programming_mode": self._programming_mode, + "ready": self._ready, + "zone_bypassed": self._zone_bypassed, + "code_arm_required": self._code_arm_required, + } + + def alarm_disarm(self, code=None): + """Send disarm command.""" + if code: + self._client.send(f"{code!s}1") + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + self._client.arm_away( + code=code, + code_arm_required=self._code_arm_required, + auto_bypass=self._auto_bypass, + ) + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + self._client.arm_home( + code=code, + code_arm_required=self._code_arm_required, + auto_bypass=self._auto_bypass, + ) + + def alarm_arm_night(self, code=None): + """Send arm night command.""" + self._client.arm_night( + code=code, + code_arm_required=self._code_arm_required, + alt_night_mode=self._alt_night_mode, + auto_bypass=self._auto_bypass, + ) + + def alarm_toggle_chime(self, code=None): + """Send toggle chime command.""" + if code: + self._client.send(f"{code!s}9") + + def alarm_keypress(self, keypress): + """Send custom keypresses.""" + if keypress: + self._client.send(keypress) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/binary_sensor.py new file mode 100644 index 00000000000..71bcc399e08 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/binary_sensor.py @@ -0,0 +1,177 @@ +"""Support for AlarmDecoder zone states- represented as binary sensors.""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import ( + CONF_RELAY_ADDR, + CONF_RELAY_CHAN, + CONF_ZONE_LOOP, + CONF_ZONE_NAME, + CONF_ZONE_NUMBER, + CONF_ZONE_RFID, + CONF_ZONE_TYPE, + DEFAULT_ZONE_OPTIONS, + OPTIONS_ZONES, + SIGNAL_REL_MESSAGE, + SIGNAL_RFX_MESSAGE, + SIGNAL_ZONE_FAULT, + SIGNAL_ZONE_RESTORE, +) + +_LOGGER = logging.getLogger(__name__) + +ATTR_RF_BIT0 = "rf_bit0" +ATTR_RF_LOW_BAT = "rf_low_battery" +ATTR_RF_SUPERVISED = "rf_supervised" +ATTR_RF_BIT3 = "rf_bit3" +ATTR_RF_LOOP3 = "rf_loop3" +ATTR_RF_LOOP2 = "rf_loop2" +ATTR_RF_LOOP4 = "rf_loop4" +ATTR_RF_LOOP1 = "rf_loop1" + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): + """Set up for AlarmDecoder sensor.""" + + zones = entry.options.get(OPTIONS_ZONES, DEFAULT_ZONE_OPTIONS) + + entities = [] + for zone_num in zones: + zone_info = zones[zone_num] + zone_type = zone_info[CONF_ZONE_TYPE] + zone_name = zone_info[CONF_ZONE_NAME] + zone_rfid = zone_info.get(CONF_ZONE_RFID) + zone_loop = zone_info.get(CONF_ZONE_LOOP) + relay_addr = zone_info.get(CONF_RELAY_ADDR) + relay_chan = zone_info.get(CONF_RELAY_CHAN) + entity = AlarmDecoderBinarySensor( + zone_num, zone_name, zone_type, zone_rfid, zone_loop, relay_addr, relay_chan + ) + entities.append(entity) + + async_add_entities(entities) + + +class AlarmDecoderBinarySensor(BinarySensorEntity): + """Representation of an AlarmDecoder binary sensor.""" + + def __init__( + self, + zone_number, + zone_name, + zone_type, + zone_rfid, + zone_loop, + relay_addr, + relay_chan, + ): + """Initialize the binary_sensor.""" + self._zone_number = int(zone_number) + self._zone_type = zone_type + self._state = None + self._name = zone_name + self._rfid = zone_rfid + self._loop = zone_loop + self._rfstate = None + self._relay_addr = relay_addr + self._relay_chan = relay_chan + + async def async_added_to_hass(self): + """Register callbacks.""" + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_ZONE_FAULT, self._fault_callback + ) + ) + + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_ZONE_RESTORE, self._restore_callback + ) + ) + + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_RFX_MESSAGE, self._rfx_message_callback + ) + ) + + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_REL_MESSAGE, self._rel_message_callback + ) + ) + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + attr = {CONF_ZONE_NUMBER: self._zone_number} + if self._rfid and self._rfstate is not None: + attr[ATTR_RF_BIT0] = bool(self._rfstate & 0x01) + attr[ATTR_RF_LOW_BAT] = bool(self._rfstate & 0x02) + attr[ATTR_RF_SUPERVISED] = bool(self._rfstate & 0x04) + attr[ATTR_RF_BIT3] = bool(self._rfstate & 0x08) + attr[ATTR_RF_LOOP3] = bool(self._rfstate & 0x10) + attr[ATTR_RF_LOOP2] = bool(self._rfstate & 0x20) + attr[ATTR_RF_LOOP4] = bool(self._rfstate & 0x40) + attr[ATTR_RF_LOOP1] = bool(self._rfstate & 0x80) + return attr + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._state == 1 + + @property + def device_class(self): + """Return the class of this sensor, from DEVICE_CLASSES.""" + return self._zone_type + + def _fault_callback(self, zone): + """Update the zone's state, if needed.""" + if zone is None or int(zone) == self._zone_number: + self._state = 1 + 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._state = 0 + self.schedule_update_ha_state() + + def _rfx_message_callback(self, message): + """Update RF state.""" + if self._rfid and message and message.serial_number == self._rfid: + self._rfstate = message.value + if self._loop: + self._state = 1 if message.loop[self._loop - 1] else 0 + self.schedule_update_ha_state() + + def _rel_message_callback(self, message): + """Update relay / expander state.""" + + if self._relay_addr == message.address and self._relay_chan == message.channel: + _LOGGER.debug( + "%s %d:%d value:%d", + "Relay" if message.type == message.RELAY else "ZoneExpander", + message.address, + message.channel, + message.value, + ) + self._state = message.value + self.schedule_update_ha_state() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/config_flow.py new file mode 100644 index 00000000000..1c46f50f3cb --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/config_flow.py @@ -0,0 +1,365 @@ +"""Config flow for AlarmDecoder.""" +import logging + +from adext import AdExt +from alarmdecoder.devices import SerialDevice, SocketDevice +from alarmdecoder.util import NoDeviceError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.binary_sensor import DEVICE_CLASSES +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL +from homeassistant.core import callback + +from .const import ( + CONF_ALT_NIGHT_MODE, + CONF_AUTO_BYPASS, + CONF_CODE_ARM_REQUIRED, + CONF_DEVICE_BAUD, + CONF_DEVICE_PATH, + CONF_RELAY_ADDR, + CONF_RELAY_CHAN, + CONF_ZONE_LOOP, + CONF_ZONE_NAME, + CONF_ZONE_NUMBER, + CONF_ZONE_RFID, + CONF_ZONE_TYPE, + DEFAULT_ARM_OPTIONS, + DEFAULT_DEVICE_BAUD, + DEFAULT_DEVICE_HOST, + DEFAULT_DEVICE_PATH, + DEFAULT_DEVICE_PORT, + DEFAULT_ZONE_OPTIONS, + DEFAULT_ZONE_TYPE, + DOMAIN, + OPTIONS_ARM, + OPTIONS_ZONES, + PROTOCOL_SERIAL, + PROTOCOL_SOCKET, +) + +EDIT_KEY = "edit_selection" +EDIT_ZONES = "Zones" +EDIT_SETTINGS = "Arming Settings" + +_LOGGER = logging.getLogger(__name__) + + +class AlarmDecoderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a AlarmDecoder config flow.""" + + VERSION = 1 + + def __init__(self): + """Initialize AlarmDecoder ConfigFlow.""" + self.protocol = None + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for AlarmDecoder.""" + return AlarmDecoderOptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if user_input is not None: + self.protocol = user_input[CONF_PROTOCOL] + return await self.async_step_protocol() + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_PROTOCOL): vol.In( + [PROTOCOL_SOCKET, PROTOCOL_SERIAL] + ), + } + ), + ) + + async def async_step_protocol(self, user_input=None): + """Handle AlarmDecoder protocol setup.""" + errors = {} + if user_input is not None: + if _device_already_added( + self._async_current_entries(), user_input, self.protocol + ): + return self.async_abort(reason="already_configured") + connection = {} + baud = None + if self.protocol == PROTOCOL_SOCKET: + host = connection[CONF_HOST] = user_input[CONF_HOST] + port = connection[CONF_PORT] = user_input[CONF_PORT] + title = f"{host}:{port}" + device = SocketDevice(interface=(host, port)) + if self.protocol == PROTOCOL_SERIAL: + path = connection[CONF_DEVICE_PATH] = user_input[CONF_DEVICE_PATH] + baud = connection[CONF_DEVICE_BAUD] = user_input[CONF_DEVICE_BAUD] + title = path + device = SerialDevice(interface=path) + + controller = AdExt(device) + + def test_connection(): + controller.open(baud) + controller.close() + + try: + await self.hass.async_add_executor_job(test_connection) + return self.async_create_entry( + title=title, data={CONF_PROTOCOL: self.protocol, **connection} + ) + except NoDeviceError: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception during AlarmDecoder setup") + errors["base"] = "unknown" + + if self.protocol == PROTOCOL_SOCKET: + schema = vol.Schema( + { + vol.Required(CONF_HOST, default=DEFAULT_DEVICE_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_DEVICE_PORT): int, + } + ) + if self.protocol == PROTOCOL_SERIAL: + schema = vol.Schema( + { + vol.Required(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): str, + vol.Required(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): int, + } + ) + + return self.async_show_form( + step_id="protocol", + data_schema=schema, + errors=errors, + ) + + +class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow): + """Handle AlarmDecoder options.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize AlarmDecoder options flow.""" + self.arm_options = config_entry.options.get(OPTIONS_ARM, DEFAULT_ARM_OPTIONS) + self.zone_options = config_entry.options.get( + OPTIONS_ZONES, DEFAULT_ZONE_OPTIONS + ) + self.selected_zone = None + + async def async_step_init(self, user_input=None): + """Manage the options.""" + if user_input is not None: + if user_input[EDIT_KEY] == EDIT_SETTINGS: + return await self.async_step_arm_settings() + if user_input[EDIT_KEY] == EDIT_ZONES: + return await self.async_step_zone_select() + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required(EDIT_KEY, default=EDIT_SETTINGS): vol.In( + [EDIT_SETTINGS, EDIT_ZONES] + ) + }, + ), + ) + + async def async_step_arm_settings(self, user_input=None): + """Arming options form.""" + if user_input is not None: + return self.async_create_entry( + title="", + data={OPTIONS_ARM: user_input, OPTIONS_ZONES: self.zone_options}, + ) + + return self.async_show_form( + step_id="arm_settings", + data_schema=vol.Schema( + { + vol.Optional( + CONF_ALT_NIGHT_MODE, + default=self.arm_options[CONF_ALT_NIGHT_MODE], + ): bool, + vol.Optional( + CONF_AUTO_BYPASS, default=self.arm_options[CONF_AUTO_BYPASS] + ): bool, + vol.Optional( + CONF_CODE_ARM_REQUIRED, + default=self.arm_options[CONF_CODE_ARM_REQUIRED], + ): bool, + }, + ), + ) + + async def async_step_zone_select(self, user_input=None): + """Zone selection form.""" + errors = _validate_zone_input(user_input) + + if user_input is not None and not errors: + self.selected_zone = str( + int(user_input[CONF_ZONE_NUMBER]) + ) # remove leading zeros + return await self.async_step_zone_details() + + return self.async_show_form( + step_id="zone_select", + data_schema=vol.Schema({vol.Required(CONF_ZONE_NUMBER): str}), + errors=errors, + ) + + async def async_step_zone_details(self, user_input=None): + """Zone details form.""" + errors = _validate_zone_input(user_input) + + if user_input is not None and not errors: + zone_options = self.zone_options.copy() + zone_id = self.selected_zone + zone_options[zone_id] = _fix_input_types(user_input) + + # Delete zone entry if zone_name is omitted + if CONF_ZONE_NAME not in zone_options[zone_id]: + zone_options.pop(zone_id) + + return self.async_create_entry( + title="", + data={OPTIONS_ARM: self.arm_options, OPTIONS_ZONES: zone_options}, + ) + + existing_zone_settings = self.zone_options.get(self.selected_zone, {}) + + return self.async_show_form( + step_id="zone_details", + description_placeholders={CONF_ZONE_NUMBER: self.selected_zone}, + data_schema=vol.Schema( + { + vol.Optional( + CONF_ZONE_NAME, + description={ + "suggested_value": existing_zone_settings.get( + CONF_ZONE_NAME + ) + }, + ): str, + vol.Optional( + CONF_ZONE_TYPE, + default=existing_zone_settings.get( + CONF_ZONE_TYPE, DEFAULT_ZONE_TYPE + ), + ): vol.In(DEVICE_CLASSES), + vol.Optional( + CONF_ZONE_RFID, + description={ + "suggested_value": existing_zone_settings.get( + CONF_ZONE_RFID + ) + }, + ): str, + vol.Optional( + CONF_ZONE_LOOP, + description={ + "suggested_value": existing_zone_settings.get( + CONF_ZONE_LOOP + ) + }, + ): str, + vol.Optional( + CONF_RELAY_ADDR, + description={ + "suggested_value": existing_zone_settings.get( + CONF_RELAY_ADDR + ) + }, + ): str, + vol.Optional( + CONF_RELAY_CHAN, + description={ + "suggested_value": existing_zone_settings.get( + CONF_RELAY_CHAN + ) + }, + ): str, + } + ), + errors=errors, + ) + + +def _validate_zone_input(zone_input): + if not zone_input: + return {} + errors = {} + + # CONF_RELAY_ADDR & CONF_RELAY_CHAN are inclusive + if (CONF_RELAY_ADDR in zone_input and CONF_RELAY_CHAN not in zone_input) or ( + CONF_RELAY_ADDR not in zone_input and CONF_RELAY_CHAN in zone_input + ): + errors["base"] = "relay_inclusive" + + # The following keys must be int + for key in [CONF_ZONE_NUMBER, CONF_ZONE_LOOP, CONF_RELAY_ADDR, CONF_RELAY_CHAN]: + if key in zone_input: + try: + int(zone_input[key]) + except ValueError: + errors[key] = "int" + + # CONF_ZONE_LOOP depends on CONF_ZONE_RFID + if CONF_ZONE_LOOP in zone_input and CONF_ZONE_RFID not in zone_input: + errors[CONF_ZONE_LOOP] = "loop_rfid" + + # CONF_ZONE_LOOP must be 1-4 + if ( + CONF_ZONE_LOOP in zone_input + and zone_input[CONF_ZONE_LOOP].isdigit() + and int(zone_input[CONF_ZONE_LOOP]) not in list(range(1, 5)) + ): + errors[CONF_ZONE_LOOP] = "loop_range" + + return errors + + +def _fix_input_types(zone_input): + """Convert necessary keys to int. + + Since ConfigFlow inputs of type int cannot default to an empty string, we collect the values below as + strings and then convert them to ints. + """ + + for key in [CONF_ZONE_LOOP, CONF_RELAY_ADDR, CONF_RELAY_CHAN]: + if key in zone_input: + zone_input[key] = int(zone_input[key]) + + return zone_input + + +def _device_already_added(current_entries, user_input, protocol): + """Determine if entry has already been added to HA.""" + user_host = user_input.get(CONF_HOST) + user_port = user_input.get(CONF_PORT) + user_path = user_input.get(CONF_DEVICE_PATH) + user_baud = user_input.get(CONF_DEVICE_BAUD) + + for entry in current_entries: + entry_host = entry.data.get(CONF_HOST) + entry_port = entry.data.get(CONF_PORT) + entry_path = entry.data.get(CONF_DEVICE_PATH) + entry_baud = entry.data.get(CONF_DEVICE_BAUD) + + if ( + protocol == PROTOCOL_SOCKET + and user_host == entry_host + and user_port == entry_port + ): + return True + + if ( + protocol == PROTOCOL_SERIAL + and user_baud == entry_baud + and user_path == entry_path + ): + return True + + return False diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/const.py new file mode 100644 index 00000000000..f1bfb66f0d4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/const.py @@ -0,0 +1,49 @@ +"""Constants for the AlarmDecoder component.""" + +CONF_ALT_NIGHT_MODE = "alt_night_mode" +CONF_AUTO_BYPASS = "auto_bypass" +CONF_CODE_ARM_REQUIRED = "code_arm_required" +CONF_DEVICE_BAUD = "device_baudrate" +CONF_DEVICE_PATH = "device_path" +CONF_RELAY_ADDR = "zone_relayaddr" +CONF_RELAY_CHAN = "zone_relaychan" +CONF_ZONE_LOOP = "zone_loop" +CONF_ZONE_NAME = "zone_name" +CONF_ZONE_NUMBER = "zone_number" +CONF_ZONE_RFID = "zone_rfid" +CONF_ZONE_TYPE = "zone_type" + +DATA_AD = "alarmdecoder" +DATA_REMOVE_STOP_LISTENER = "rm_stop_listener" +DATA_REMOVE_UPDATE_LISTENER = "rm_update_listener" +DATA_RESTART = "restart" + +DEFAULT_ALT_NIGHT_MODE = False +DEFAULT_AUTO_BYPASS = False +DEFAULT_CODE_ARM_REQUIRED = True +DEFAULT_DEVICE_BAUD = 115200 +DEFAULT_DEVICE_HOST = "alarmdecoder" +DEFAULT_DEVICE_PATH = "/dev/ttyUSB0" +DEFAULT_DEVICE_PORT = 10000 +DEFAULT_ZONE_TYPE = "window" + +DEFAULT_ARM_OPTIONS = { + CONF_ALT_NIGHT_MODE: DEFAULT_ALT_NIGHT_MODE, + CONF_AUTO_BYPASS: DEFAULT_AUTO_BYPASS, + CONF_CODE_ARM_REQUIRED: DEFAULT_CODE_ARM_REQUIRED, +} +DEFAULT_ZONE_OPTIONS = {} + +DOMAIN = "alarmdecoder" + +OPTIONS_ARM = "arm_options" +OPTIONS_ZONES = "zone_options" + +PROTOCOL_SERIAL = "serial" +PROTOCOL_SOCKET = "socket" + +SIGNAL_PANEL_MESSAGE = "alarmdecoder.panel_message" +SIGNAL_REL_MESSAGE = "alarmdecoder.rel_message" +SIGNAL_RFX_MESSAGE = "alarmdecoder.rfx_message" +SIGNAL_ZONE_FAULT = "alarmdecoder.zone_fault" +SIGNAL_ZONE_RESTORE = "alarmdecoder.zone_restore" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/manifest.json new file mode 100644 index 00000000000..fa2bcca389f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "alarmdecoder", + "name": "AlarmDecoder", + "documentation": "https://www.home-assistant.io/integrations/alarmdecoder", + "requirements": ["adext==0.4.1"], + "codeowners": ["@ajschmidt8"], + "config_flow": true, + "iot_class": "local_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/sensor.py new file mode 100644 index 00000000000..e3c85cb5893 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/sensor.py @@ -0,0 +1,60 @@ +"""Support for AlarmDecoder sensors (Shows Panel Display).""" +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import SIGNAL_PANEL_MESSAGE + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): + """Set up for AlarmDecoder sensor.""" + + entity = AlarmDecoderSensor() + async_add_entities([entity]) + return True + + +class AlarmDecoderSensor(SensorEntity): + """Representation of an AlarmDecoder keypad.""" + + def __init__(self): + """Initialize the alarm panel.""" + self._display = "" + self._state = None + self._icon = "mdi:alarm-check" + self._name = "Alarm Panel Display" + + async def async_added_to_hass(self): + """Register callbacks.""" + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_PANEL_MESSAGE, self._message_callback + ) + ) + + def _message_callback(self, message): + if self._display != message.text: + self._display = message.text + self.schedule_update_ha_state() + + @property + def icon(self): + """Return the icon if any.""" + return self._icon + + @property + def state(self): + """Return the overall state.""" + return self._display + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/services.yaml b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/services.yaml new file mode 100644 index 00000000000..9d50eae07e6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/services.yaml @@ -0,0 +1,31 @@ +alarm_keypress: + name: Key press + description: Send custom keypresses to the alarm. + target: + entity: + integration: alarmdecoder + domain: alarm_control_panel + fields: + keypress: + name: Key press + description: "String to send to the alarm panel." + required: true + example: "*71" + selector: + text: + +alarm_toggle_chime: + name: Toggle Chime + description: Send the alarm the toggle chime command. + target: + entity: + integration: alarmdecoder + domain: alarm_control_panel + fields: + code: + name: Code + description: A code to toggle the alarm control panel chime with. + required: true + example: 1234 + selector: + text: diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/strings.json new file mode 100644 index 00000000000..33b33749048 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/strings.json @@ -0,0 +1,72 @@ +{ + "config": { + "step": { + "user": { + "title": "Choose AlarmDecoder Protocol", + "data": { + "protocol": "Protocol" + } + }, + "protocol": { + "title": "Configure connection settings", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "device_baudrate": "Device Baud Rate", + "device_path": "Device Path" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "create_entry": { "default": "Successfully connected to AlarmDecoder." }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "options": { + "step": { + "init": { + "title": "Configure AlarmDecoder", + "description": "What would you like to edit?", + "data": { + "edit_select": "Edit" + } + }, + "arm_settings": { + "title": "Configure AlarmDecoder", + "data": { + "auto_bypass": "Auto Bypass on Arm", + "code_arm_required": "Code Required for Arming", + "alt_night_mode": "Alternative Night Mode" + } + }, + "zone_select": { + "title": "Configure AlarmDecoder", + "description": "Enter the zone number you'd like to to add, edit, or remove.", + "data": { + "zone_number": "Zone Number" + } + }, + "zone_details": { + "title": "Configure AlarmDecoder", + "description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave Zone Name blank.", + "data": { + "zone_name": "Zone Name", + "zone_type": "Zone Type", + "zone_rfid": "RF Serial", + "zone_loop": "RF Loop", + "zone_relayaddr": "Relay Address", + "zone_relaychan": "Relay Channel" + } + } + }, + "error": { + "relay_inclusive": "Relay Address and Relay Channel are codependent and must be included together.", + "int": "The field below must be an integer.", + "loop_rfid": "RF Loop cannot be used without RF Serial.", + "loop_range": "RF Loop must be an integer between 1 and 4." + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/ca.json new file mode 100644 index 00000000000..da882bb614d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/ca.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "create_entry": { + "default": "S'ha connectat correctament amb AlarmDecoder." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "Velocitat, en baudis, del dispositiu", + "device_path": "Ruta del dispositiu", + "host": "Amfitri\u00f3", + "port": "Port" + }, + "title": "Configuraci\u00f3 dels par\u00e0metres de connexi\u00f3" + }, + "user": { + "data": { + "protocol": "Protocol" + }, + "title": "Selecciona el protocol d'AlarmDecoder" + } + } + }, + "options": { + "error": { + "int": "El camp seg\u00fcent ha de ser un nombre enter.", + "loop_range": "El bucle RF ha de ser un nombre enter entre 1 i 4.", + "loop_rfid": "El bucle RF no es pot utilitzar sense RF s\u00e8rie.", + "relay_inclusive": "L'adre\u00e7a i el canal de rel\u00e9 s\u00f3n codependents i s'han d'incloure junts." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Mode nocturn alternatiu", + "auto_bypass": "Bypass autom\u00e0tic en l'activaci\u00f3", + "code_arm_required": "Codi necessari per a l'activaci\u00f3" + }, + "title": "Configuraci\u00f3 d'AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "Edita" + }, + "description": "Qu\u00e8 voldries editar?", + "title": "Configuraci\u00f3 d'AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "Bucle RF", + "zone_name": "Nom de la zona", + "zone_relayaddr": "Adre\u00e7a del rel\u00e9", + "zone_relaychan": "Canal del rel\u00e9", + "zone_rfid": "RF s\u00e8rie", + "zone_type": "Tipus de zona" + }, + "description": "Introdueix els detalls de la zona {zone_number}. Per suprimir la zona {zone_number}, deixa el nom de la zona en blanc.", + "title": "Configuraci\u00f3 d'AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "N\u00famero de zona" + }, + "description": "Introdueix el n\u00famero de zona que vulguis afegir, editar o eliminar.", + "title": "Configuraci\u00f3 d'AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/cs.json new file mode 100644 index 00000000000..9bbef349d5a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/cs.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "create_entry": { + "default": "\u00dasp\u011b\u0161n\u011b p\u0159ipojen k AlarmDecoder." + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "protocol": { + "data": { + "device_path": "Cesta k za\u0159\u00edzen\u00ed", + "host": "Hostitel", + "port": "Port" + }, + "title": "Nastavte spojen\u00ed" + }, + "user": { + "data": { + "protocol": "Protokol" + }, + "title": "Vyberte protokol AlarmDecoder" + } + } + }, + "options": { + "error": { + "int": "Pole n\u00ed\u017ee mus\u00ed b\u00fdt cel\u00e9 \u010d\u00edslo." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Alternativn\u00ed no\u010dn\u00ed re\u017eim", + "code_arm_required": "K\u00f3d vy\u017eadovan\u00fd pro zabezpe\u010den\u00ed" + }, + "title": "Konfigurovat AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "Upravit" + }, + "description": "Co chcete upravit?", + "title": "Konfigurovat AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "RF Loop", + "zone_name": "N\u00e1zev z\u00f3ny", + "zone_relayaddr": "Relay adresa", + "zone_relaychan": "Relay kan\u00e1l", + "zone_rfid": "RF Serial", + "zone_type": "Typ z\u00f3ny" + }, + "description": "Zadejte podrobnosti pro z\u00f3nu {zone_number}. Chcete-li odstranit z\u00f3nu {zone_number}, ponechejte n\u00e1zev z\u00f3ny pr\u00e1zdn\u00fd.", + "title": "Konfigurovat AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "\u010c\u00edslo z\u00f3ny" + }, + "description": "Zadejte \u010d\u00edslo z\u00f3ny, kterou chcete p\u0159idat, upravit nebo odstranit.", + "title": "Konfigurovat AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/de.json new file mode 100644 index 00000000000..aea85f49a59 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/de.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "create_entry": { + "default": "Erfolgreich mit AlarmDecoder verbunden." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "Ger\u00e4te-Baudrate", + "device_path": "Ger\u00e4tepfad", + "host": "Host", + "port": "Port" + } + }, + "user": { + "data": { + "protocol": "Protokoll" + } + } + } + }, + "options": { + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Alternativer Nachtmodus" + } + }, + "init": { + "data": { + "edit_select": "Bearbeiten" + }, + "description": "Was m\u00f6chtest du bearbeiten?" + }, + "zone_details": { + "data": { + "zone_name": "Zonenname", + "zone_relayaddr": "Relais-Adresse", + "zone_type": "Zonentyp" + } + }, + "zone_select": { + "data": { + "zone_number": "Zonennummer" + }, + "description": "Gib die die Zonennummer ein, die du hinzuf\u00fcgen, bearbeiten oder entfernen m\u00f6chtest." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/el.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/el.json new file mode 100644 index 00000000000..7c3b0b6737c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/el.json @@ -0,0 +1,71 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03b9\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7" + }, + "create_entry": { + "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf AlarmDecoder." + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "\u03a1\u03c5\u03b8\u03bc\u03cc\u03c2 Baud \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", + "device_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", + "host": "\u0394\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1" + }, + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "user": { + "data": { + "protocol": "\u03a0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf AlarmDecoder" + } + } + }, + "options": { + "error": { + "int": "\u03a4\u03bf \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c0\u03b5\u03b4\u03af\u03bf \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03ba\u03ad\u03c1\u03b1\u03b9\u03bf\u03c2.", + "loop_range": "\u039f \u03b2\u03c1\u03cc\u03c7\u03bf\u03c2 RF \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03ba\u03ad\u03c1\u03b1\u03b9\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd 1 \u03ba\u03b1\u03b9 4.", + "loop_rfid": "\u039f \u03b2\u03c1\u03cc\u03c7\u03bf\u03c2 RF \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03c7\u03c9\u03c1\u03af\u03c2 \u03c4\u03bf \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc RF.", + "relay_inclusive": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c1\u03b5\u03bb\u03ad \u03ba\u03b1\u03b9 \u03c4\u03bf \u03ba\u03b1\u03bd\u03ac\u03bb\u03b9 \u03b1\u03bd\u03b1\u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7\u03c2 \u03b5\u03be\u03b1\u03c1\u03c4\u03ce\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03ba\u03ce\u03b4\u03b9\u03ba\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b5\u03c1\u03b9\u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b1\u03b6\u03af." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "\u0395\u03bd\u03b1\u03bb\u03bb\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae \u03bd\u03c5\u03c7\u03c4\u03b5\u03c1\u03b9\u03bd\u03ae \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1", + "auto_bypass": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03c3\u03c4\u03bf\u03bd \u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03cc", + "code_arm_required": "\u0391\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b3\u03b9\u03b1 \u03bf\u03c0\u03bb\u03b9\u03c3\u03bc\u03cc" + }, + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "\u0395\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b1" + }, + "description": "\u03a4\u03b9 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5;", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "\u0392\u03c1\u03cc\u03c7\u03bf\u03c2 RF", + "zone_name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03b6\u03ce\u03bd\u03b7\u03c2", + "zone_relayaddr": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c1\u03b5\u03bb\u03ad", + "zone_relaychan": "\u039a\u03b1\u03bd\u03ac\u03bb\u03b9 \u03c1\u03b5\u03bb\u03ad", + "zone_rfid": "\u03a3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc RF", + "zone_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03b6\u03ce\u03bd\u03b7\u03c2" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03b6\u03ce\u03bd\u03b7 {zone_number}. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03b3\u03c1\u03ac\u03c8\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b6\u03ce\u03bd\u03b7 {zone_number}, \u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b5\u03bd\u03cc \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03b6\u03ce\u03bd\u03b7\u03c2.", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b6\u03ce\u03bd\u03b7\u03c2" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03b6\u03ce\u03bd\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5, \u03bd\u03b1 \u03b5\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03ae \u03bd\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5.", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/en.json new file mode 100644 index 00000000000..747ccb51f9f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/en.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "create_entry": { + "default": "Successfully connected to AlarmDecoder." + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "Device Baud Rate", + "device_path": "Device Path", + "host": "Host", + "port": "Port" + }, + "title": "Configure connection settings" + }, + "user": { + "data": { + "protocol": "Protocol" + }, + "title": "Choose AlarmDecoder Protocol" + } + } + }, + "options": { + "error": { + "int": "The field below must be an integer.", + "loop_range": "RF Loop must be an integer between 1 and 4.", + "loop_rfid": "RF Loop cannot be used without RF Serial.", + "relay_inclusive": "Relay Address and Relay Channel are codependent and must be included together." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Alternative Night Mode", + "auto_bypass": "Auto Bypass on Arm", + "code_arm_required": "Code Required for Arming" + }, + "title": "Configure AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "Edit" + }, + "description": "What would you like to edit?", + "title": "Configure AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "RF Loop", + "zone_name": "Zone Name", + "zone_relayaddr": "Relay Address", + "zone_relaychan": "Relay Channel", + "zone_rfid": "RF Serial", + "zone_type": "Zone Type" + }, + "description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave Zone Name blank.", + "title": "Configure AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "Zone Number" + }, + "description": "Enter the zone number you'd like to to add, edit, or remove.", + "title": "Configure AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/es-419.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/es-419.json new file mode 100644 index 00000000000..2152084ea56 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/es-419.json @@ -0,0 +1,36 @@ +{ + "config": { + "create_entry": { + "default": "Conectado con \u00e9xito a AlarmDecoder." + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "Tasa de baudios del dispositivo", + "device_path": "Ruta del dispositivo" + }, + "title": "Configurar los ajustes de conexi\u00f3n" + }, + "user": { + "data": { + "protocol": "Protocolo" + }, + "title": "Elija el protocolo AlarmDecoder" + } + } + }, + "options": { + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Modo nocturno alternativo" + } + }, + "init": { + "data": { + "edit_select": "Editar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/es.json new file mode 100644 index 00000000000..5dfd7ab5745 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/es.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "create_entry": { + "default": "Conectado con \u00e9xito a AlarmDecoder." + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "Velocidad en baudios del dispositivo", + "device_path": "Ruta del dispositivo", + "host": "Host", + "port": "Puerto" + }, + "title": "Configurar los ajustes de conexi\u00f3n" + }, + "user": { + "data": { + "protocol": "Protocolo" + }, + "title": "Elige el protocolo del AlarmDecoder" + } + } + }, + "options": { + "error": { + "int": "El campo siguiente debe ser un n\u00famero entero.", + "loop_range": "El bucle RF debe ser un n\u00famero entero entre 1 y 4.", + "loop_rfid": "El bucle de RF no puede utilizarse sin el serie RF.", + "relay_inclusive": "La direcci\u00f3n de retransmisi\u00f3n y el canal de retransmisi\u00f3n son codependientes y deben incluirse a la vez." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Modo noche alternativo", + "auto_bypass": "Desv\u00edo autom\u00e1tico al armar", + "code_arm_required": "C\u00f3digo requerido para el armado" + }, + "title": "Configurar AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "Editar" + }, + "description": "\u00bfQu\u00e9 te gustar\u00eda editar?", + "title": "Configurar AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "Bucle RF", + "zone_name": "Nombre de zona", + "zone_relayaddr": "Direcci\u00f3n de retransmisi\u00f3n", + "zone_relaychan": "Canal de retransmisi\u00f3n", + "zone_rfid": "Serie RF", + "zone_type": "Tipo de zona" + }, + "description": "Introduce los detalles para la zona {zona_number}. Para borrar la zona {zone_number}, deja el nombre de la zona en blanco.", + "title": "Configurar AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "N\u00famero de zona" + }, + "description": "Introduce el n\u00famero de zona que deseas a\u00f1adir, editar o eliminar.", + "title": "Configurar AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/et.json new file mode 100644 index 00000000000..d09fb725e34 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/et.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "create_entry": { + "default": "AlarmDecoderiga \u00fchendamine \u00f5nnestus." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "Seadme edastuskiirus", + "device_path": "Seadme asukoht", + "host": "", + "port": "" + }, + "title": "M\u00e4\u00e4ra \u00fchenduse seaded" + }, + "user": { + "data": { + "protocol": "Protokoll" + }, + "title": "Vali AlarmDecoderi protokoll" + } + } + }, + "options": { + "error": { + "int": "Allolev v\u00e4li peab olema t\u00e4isarv.", + "loop_range": "RF Loop peab olema t\u00e4isarv vahemikus 1\u20134.", + "loop_rfid": "RF Loopi ei saa kasutada ilma RF Serial-ita.", + "relay_inclusive": "Releeaadress ja releekanal s\u00f5ltuvad \u00fcksteisest ja need peavad olema koos." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Alternatiivne \u00f6\u00f6re\u017eiim", + "auto_bypass": "Automaatne m\u00f6\u00f6daviik valvestamisel", + "code_arm_required": "Valvestamise kood" + }, + "title": "Seadista AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "Muuda" + }, + "description": "Mida Te soovite muuta?", + "title": "Seadista AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "RF silmus", + "zone_name": "Ala nimi", + "zone_relayaddr": "Relee aadress", + "zone_relaychan": "Relee kanalinumber", + "zone_rfid": "RF jada\u00fchendus", + "zone_type": "Ala t\u00fc\u00fcp" + }, + "description": "Sisesta ala {zone_number} \u00fcksikasjad. Ala {zone_number} kustutamiseks j\u00e4ta ala nimi t\u00fchjaks.", + "title": "Seadista AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "Ala number" + }, + "description": "Sisesta ala number mida soovid lisada, muuta v\u00f5i eemaldada.", + "title": "Seadista AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/fr.json new file mode 100644 index 00000000000..1d1a200503f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/fr.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "create_entry": { + "default": "Connexion r\u00e9ussie \u00e0 AlarmDecoder." + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "D\u00e9bit en bauds de l'appareil", + "device_path": "Chemin du p\u00e9riph\u00e9rique", + "host": "H\u00f4te", + "port": "Port" + }, + "title": "Configurer les param\u00e8tres de connexion" + }, + "user": { + "data": { + "protocol": "Protocole" + }, + "title": "Choisissez le protocole AlarmDecoder" + } + } + }, + "options": { + "error": { + "int": "Le champ ci-dessous doit \u00eatre un entier.", + "loop_range": "La boucle RF doit \u00eatre un entier compris entre 1 et 4.", + "loop_rfid": "La boucle RF ne peut pas \u00eatre utilis\u00e9e sans s\u00e9rie RF.", + "relay_inclusive": "L'adresse de relais et le canal de relais d\u00e9pendent du codage et doivent \u00eatre inclus ensemble." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Mode nuit alternatif", + "auto_bypass": "Bypass automatique \u00e0 l'armement", + "code_arm_required": "Code requis pour l'armement" + }, + "title": "Configurer AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "Modifier" + }, + "description": "Que voulez-vous modifier?", + "title": "Configurer AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "Boucle RF", + "zone_name": "Nom de zone", + "zone_relayaddr": "Adresse de relais", + "zone_relaychan": "Canal de relais", + "zone_rfid": "RF S\u00e9rie", + "zone_type": "Type de zone" + }, + "description": "Entrez les d\u00e9tails de la zone {zone_number} . Pour supprimer la zone {zone_number} , laissez le nom de zone vide.", + "title": "Configurer AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "Num\u00e9ro de zone" + }, + "description": "Saisissez le num\u00e9ro de zone que vous souhaitez ajouter, modifier ou supprimer.", + "title": "Configurer AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/hu.json new file mode 100644 index 00000000000..8c80adcb3c0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/hu.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "create_entry": { + "default": "Sikeres csatlakoz\u00e1s az AlarmDecoderhez." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "protocol": { + "data": { + "host": "Hoszt", + "port": "Port" + } + }, + "user": { + "data": { + "protocol": "Protokoll" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "edit_select": "Szerkeszt\u00e9s" + } + }, + "zone_details": { + "data": { + "zone_name": "Z\u00f3na neve" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/id.json new file mode 100644 index 00000000000..39c8282b36f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/id.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "create_entry": { + "default": "Berhasil terhubung ke AlarmDecoder." + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "Laju Baud Perangkat", + "device_path": "Jalur Perangkat", + "host": "Host", + "port": "Port" + }, + "title": "Konfigurasikan pengaturan koneksi" + }, + "user": { + "data": { + "protocol": "Protokol" + }, + "title": "Pilih Protokol AlarmDecoder" + } + } + }, + "options": { + "error": { + "int": "Bidang di bawah ini harus berupa bilangan bulat.", + "loop_range": "RF Loop harus merupakan bilangan bulat antara 1 dan 4.", + "loop_rfid": "RF Loop tidak dapat digunakan tanpa RF Serial.", + "relay_inclusive": "Relay Address dan Relay Channel saling tergantung dan harus disertakan bersama-sama." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Mode Malam Alternatif", + "auto_bypass": "Diaktifkan Secara Otomatis", + "code_arm_required": "Kode Diperlukan untuk Mengaktifkan" + }, + "title": "Konfigurasikan AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "Edit" + }, + "description": "Apa yang ingin diedit?", + "title": "Konfigurasikan AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "RF Loop", + "zone_name": "Nama Zona", + "zone_relayaddr": "Relay Address", + "zone_relaychan": "Relay Channel", + "zone_rfid": "RF Serial", + "zone_type": "Jenis Zona" + }, + "description": "Masukkan detail untuk zona {zone_number}. Untuk menghapus zona {zone_number}, kosongkan Nama Zona.", + "title": "Konfigurasikan AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "Nomor Zona" + }, + "description": "Masukkan nomor zona yang ingin ditambahkan, diedit, atau dihapus.", + "title": "Konfigurasikan AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/it.json new file mode 100644 index 00000000000..70be8d733fe --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/it.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "create_entry": { + "default": "Collegato con successo ad AlarmDecoder." + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "Velocit\u00e0 di trasmissione del dispositivo", + "device_path": "Percorso del dispositivo", + "host": "Host", + "port": "Porta" + }, + "title": "Configurare le impostazioni di connessione" + }, + "user": { + "data": { + "protocol": "Protocollo" + }, + "title": "Scegliere il protocollo AlarmDecoder" + } + } + }, + "options": { + "error": { + "int": "Il campo sottostante deve essere un numero intero.", + "loop_range": "Il Ciclo RF deve essere un numero intero compreso tra 1 e 4.", + "loop_rfid": "Il Ciclo RF non pu\u00f2 essere utilizzato senza il Seriale RF ", + "relay_inclusive": "L'indirizzo del rel\u00e8 e il canale del rel\u00e8 sono codipendenti e devono essere inclusi insieme." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Modalit\u00e0 notturna alternativa", + "auto_bypass": "Bypass automatico all'attivazione", + "code_arm_required": "Codice richiesto per l'attivazione" + }, + "title": "Configurare AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "Modifica" + }, + "description": "Cosa vorresti modificare?", + "title": "Configurare AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "Ciclo RF", + "zone_name": "Nome zona", + "zone_relayaddr": "Indirizzo rel\u00e8", + "zone_relaychan": "Canale rel\u00e8", + "zone_rfid": "Seriale RF", + "zone_type": "Tipo di zona" + }, + "description": "Immettere i dettagli per la zona {zone_number}. Per eliminare la zona {zone_number}, lasciare vuoto il campo Nome zona.", + "title": "Configurare AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "Numero di zona" + }, + "description": "Immettere il numero di zona che si desidera aggiungere, modificare o rimuovere.", + "title": "Configurare AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/ko.json new file mode 100644 index 00000000000..cdb63a01bfb --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/ko.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "create_entry": { + "default": "AlarmDecoder\uc5d0 \uc131\uacf5\uc801\uc73c\ub85c \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "\uae30\uae30 \uc804\uc1a1 \uc18d\ub3c4", + "device_path": "\uae30\uae30 \uacbd\ub85c", + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8" + }, + "title": "\uc5f0\uacb0 \uc124\uc815 \uad6c\uc131\ud558\uae30" + }, + "user": { + "data": { + "protocol": "\ud504\ub85c\ud1a0\ucf5c" + }, + "title": "AlarmDecoder \ud504\ub85c\ud1a0\ucf5c \uc120\ud0dd\ud558\uae30" + } + } + }, + "options": { + "error": { + "int": "\uc544\ub798 \ud544\ub4dc\ub294 \uc815\uc218\uc5ec\uc57c \ud569\ub2c8\ub2e4.", + "loop_range": "RF \ub8e8\ud504\ub294 1\uacfc 4 \uc0ac\uc774\uc758 \uc815\uc218\uc5ec\uc57c \ud569\ub2c8\ub2e4.", + "loop_rfid": "RF \ub8e8\ud504\ub294 RF \uc2dc\ub9ac\uc5bc \uc5c6\uc73c\uba74 \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "relay_inclusive": "\ub9b4\ub808\uc774 \uc8fc\uc18c\uc640 \ub9b4\ub808\uc774 \ucc44\ub110\uc740 \uc0c1\ud638 \uc758\uc874\uc801\uc774\uae30 \ub54c\ubb38\uc5d0 \ud568\uaed8 \ud3ec\ud568\ub418\uc5b4\uc57c \ud569\ub2c8\ub2e4." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "\ub300\uccb4 \uc57c\uac04 \ubaa8\ub4dc", + "auto_bypass": "\uacbd\ube44 \uc911 \uc790\ub3d9 \uc6b0\ud68c", + "code_arm_required": "\uacbd\ube44\uc5d0 \ud544\uc694\ud55c \ucf54\ub4dc" + }, + "title": "AlarmDecoder \uad6c\uc131\ud558\uae30" + }, + "init": { + "data": { + "edit_select": "\ud3b8\uc9d1\ud558\uae30" + }, + "description": "\ubb34\uc5c7\uc744 \ud3b8\uc9d1\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "AlarmDecoder \uad6c\uc131\ud558\uae30" + }, + "zone_details": { + "data": { + "zone_loop": "RF \ub8e8\ud504", + "zone_name": "\uad6c\uc5ed \uc774\ub984", + "zone_relayaddr": "\ub9b4\ub808\uc774 \uc8fc\uc18c", + "zone_relaychan": "\ub9b4\ub808\uc774 \ucc44\ub110", + "zone_rfid": "RF \uc2dc\ub9ac\uc5bc", + "zone_type": "\uad6c\uc5ed \uc720\ud615" + }, + "description": "\uad6c\uc5ed {zone_number}\uc5d0 \ub300\ud55c \uc138\ubd80 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \uad6c\uc5ed {zone_number}\uc744(\ub97c) \uc0ad\uc81c\ud558\ub824\uba74 \uad6c\uc5ed \uc774\ub984\uc744 \ube44\uc6cc\ub450\uc138\uc694.", + "title": "AlarmDecoder \uad6c\uc131\ud558\uae30" + }, + "zone_select": { + "data": { + "zone_number": "\uad6c\uc5ed \ubc88\ud638" + }, + "description": "\ucd94\uac00, \ud3b8\uc9d1 \ub610\ub294 \uc81c\uac70\ud560 \uad6c\uc5ed \ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "AlarmDecoder \uad6c\uc131\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/lb.json new file mode 100644 index 00000000000..2df09bda41a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/lb.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "create_entry": { + "default": "Erfollegr\u00e4ich mat Alarmdecoder verbonnen." + }, + "error": { + "cannot_connect": "Feeler beim verbannen" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "Apparat Baudrate", + "device_path": "Pad vum Apparat", + "host": "Host", + "port": "Port" + }, + "title": "Verbindung's Optioune konfigur\u00e9ieren" + }, + "user": { + "data": { + "protocol": "Protokoll" + }, + "title": "Alarmdecoder Protokoll auswielen" + } + } + }, + "options": { + "error": { + "int": "D'Feld hei \u00ebnnen muss eng ganz Zuel sinn.", + "loop_range": "RF Loop muss eng ganz Zuel t\u00ebscht 1 a 4 sinn.", + "loop_rfid": "RF Loop kann net ouni RF Serial benotzt ginn.", + "relay_inclusive": "Relais Adress a Relais Kanal sin vuneneen ofh\u00e4ngeg a musse mat abegraff sinn." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Alternative Nuecht Modus", + "auto_bypass": "Auto Bypass beim aktiv\u00e9ieren", + "code_arm_required": "Code erfuerderlech fir d'Aktiv\u00e9ierung" + }, + "title": "AlarmDecoder konfigur\u00e9ieren" + }, + "init": { + "data": { + "edit_select": "\u00c4nneren" + }, + "description": "Wat w\u00eblls du \u00e4nneren?", + "title": "AlarmDecoder konfigur\u00e9ieren" + }, + "zone_details": { + "data": { + "zone_loop": "RF Schleef", + "zone_name": "Numm vun der Zone", + "zone_relayaddr": "Relais Adresse", + "zone_relaychan": "Relais Kanal", + "zone_rfid": "RF Serielle", + "zone_type": "Type vun der Zone" + }, + "description": "G\u00ebff Detailer fir Zone {zone_number} an. Fir Zone {zone_number} ze l\u00e4schen, loss den Numm vun der Zone eidel.", + "title": "AlarmDecoder konfigur\u00e9ieren" + }, + "zone_select": { + "data": { + "zone_number": "Zone Nummer" + }, + "description": "G\u00ebff d'Zonennummer an d\u00e9is Du w\u00eblls b\u00e4isetzen, \u00e4nneren oder l\u00e4schen.", + "title": "AlarmDecoder konfigur\u00e9ieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/nl.json new file mode 100644 index 00000000000..1ea9cb98b56 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/nl.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "create_entry": { + "default": "Succesvol verbonden met AlarmDecoder." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "Baudrate van apparaat", + "device_path": "Apparaatpad", + "host": "Host", + "port": "Poort" + }, + "title": "Configureer de verbindingsinstellingen" + }, + "user": { + "data": { + "protocol": "Protocol" + }, + "title": "Kies AlarmDecoder Protocol" + } + } + }, + "options": { + "error": { + "int": "Het onderstaande veld moet een geheel getal zijn.", + "loop_range": "RF Lus moet een geheel getal zijn tussen 1 en 4.", + "loop_rfid": "RF Lus kan niet worden gebruikt zonder RF Serieel.", + "relay_inclusive": "Het relais-adres en het relais-kanaal zijn codeafhankelijk en moeten samen worden opgenomen." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Alternatieve nachtmodus", + "auto_bypass": "Automatische bypass bij inschakelen", + "code_arm_required": "Code vereist voor inschakelen" + }, + "title": "Configureer AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "Bewerk" + }, + "description": "Wat wilt u bewerken?", + "title": "Configureer AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "RF Lus", + "zone_name": "Zone naam", + "zone_relayaddr": "Relais Adres", + "zone_relaychan": "Relais Kanaal", + "zone_rfid": "RF Serieel", + "zone_type": "Zone Type" + }, + "description": "Voer details in voor zone {zone_number}. Om zone {zone_number} te verwijderen, laat u Zone Name leeg.", + "title": "Configureer AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "Zone nummer" + }, + "description": "Voer het zone nummer in dat u wilt toevoegen, bewerken of verwijderen.", + "title": "Configureer AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/no.json new file mode 100644 index 00000000000..b7776822e13 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/no.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "create_entry": { + "default": "Vellykket koblet til AlarmDecoder." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "Baud-hastighet for enhet", + "device_path": "Bane til enheten", + "host": "Vert", + "port": "Port" + }, + "title": "Konfigurer tilkoblingsinnstillinger" + }, + "user": { + "data": { + "protocol": "Protokoll" + }, + "title": "Velg AlarmDecoder Protokoll" + } + } + }, + "options": { + "error": { + "int": "Feltet nedenfor m\u00e5 v\u00e6re et helt tall.", + "loop_range": "RF Loop m\u00e5 v\u00e6re et heltall mellom 1 og 4.", + "loop_rfid": "RF Loop kan ikke brukes uten RF Serial.", + "relay_inclusive": "Rel\u00e9adresse og rel\u00e9kanal er kodeavhengige og m\u00e5 inkluderes sammen." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Alternativ nattmodus", + "auto_bypass": "Auto bypass p\u00e5 Arm", + "code_arm_required": "Kode kreves for tilkobling" + }, + "title": "Konfigurer AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "Rediger" + }, + "description": "Hva \u00f8nsker du \u00e5 redigere?", + "title": "Konfigurer AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "RF Loop", + "zone_name": "Sonenavn", + "zone_relayaddr": "Rel\u00e9 adresse", + "zone_relaychan": "Rel\u00e9 kanal", + "zone_rfid": "RF seriell", + "zone_type": "Sone type" + }, + "description": "Angi detaljer for sonen {zone_number}. Hvis du vil slette sonen {zone_number}, lar du Sonenavn st\u00e5 tomt.", + "title": "Konfigurer AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "Sone nummer" + }, + "description": "Angi sonenummeret du vil legge til, redigere eller fjerne.", + "title": "Konfigurer AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/pl.json new file mode 100644 index 00000000000..3fc77149178 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/pl.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "create_entry": { + "default": "Pomy\u015blnie po\u0142\u0105czono z AlarmDecoder." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "Szybko\u015b\u0107 transmisji urz\u0105dzenia (Baud Rate)", + "device_path": "\u015acie\u017cka urz\u0105dzenia", + "host": "Nazwa hosta lub adres IP", + "port": "Port" + }, + "title": "Konfiguracja ustawie\u0144 po\u0142\u0105czenia" + }, + "user": { + "data": { + "protocol": "Protok\u00f3\u0142" + }, + "title": "Wybierz protok\u00f3\u0142 AlarmDecodera" + } + } + }, + "options": { + "error": { + "int": "Poni\u017csze pole musi by\u0107 liczb\u0105 ca\u0142kowit\u0105", + "loop_range": "P\u0119tla RF (RF Loop) musi by\u0107 liczb\u0105 ca\u0142kowit\u0105 od 1 do 4", + "loop_rfid": "P\u0119tli RF (RF Loop) nie mo\u017cna u\u017cywa\u0107 bez us\u0142ugi RF Serial", + "relay_inclusive": "Adres przeka\u017anika i kana\u0142 przeka\u017anika s\u0105 wsp\u00f3\u0142zale\u017cne i musz\u0105 by\u0107 zawarte razem" + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Alternatywny tryb nocny", + "auto_bypass": "Automatyczne obej\u015bcie przy uzbrajaniu", + "code_arm_required": "Wymagaj kodu do uzbrojenia" + }, + "title": "Konfiguracja AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "Edytuj" + }, + "description": "Co chcia\u0142by\u015b edytowa\u0107?", + "title": "Konfiguracja AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "P\u0119tla RF (RF Loop)", + "zone_name": "Nazwa strefy", + "zone_relayaddr": "Adres przeka\u017anika", + "zone_relaychan": "Kana\u0142 przeka\u017anika", + "zone_rfid": "RF Serial", + "zone_type": "Rodzaj strefy" + }, + "description": "Wprowad\u017a szczeg\u00f3\u0142y dla strefy {zone_number}. Aby usun\u0105\u0107 stref\u0119 {zone_number}, pozostaw nazw\u0119 strefy pust\u0105.", + "title": "Konfiguracja AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "Numer strefy" + }, + "description": "Wprowad\u017a numer strefy, kt\u00f3r\u0105 chcesz doda\u0107, edytowa\u0107 lub usun\u0105\u0107.", + "title": "Konfiguracja AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/pt.json new file mode 100644 index 00000000000..8d6cb9a2ebf --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/pt.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "protocol": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + }, + "options": { + "step": { + "zone_details": { + "data": { + "zone_name": "Nome da Zona", + "zone_type": "Tipo de Zona" + } + }, + "zone_select": { + "data": { + "zone_number": "N\u00famero da Zona" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/ru.json new file mode 100644 index 00000000000..34fe0d9c718 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/ru.json @@ -0,0 +1,74 @@ +{ + "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." + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a AlarmDecoder." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "\u0421\u043a\u043e\u0440\u043e\u0441\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0434\u0430\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "device_path": "\u041f\u0443\u0442\u044c \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443", + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" + }, + "user": { + "data": { + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b AlarmDecoder" + } + } + }, + "options": { + "error": { + "int": "\u041f\u043e\u043b\u0435 \u043d\u0438\u0436\u0435 \u0434\u043e\u043b\u0436\u043d\u043e \u0431\u044b\u0442\u044c \u0446\u0435\u043b\u044b\u043c \u0447\u0438\u0441\u043b\u043e\u043c.", + "loop_range": "RF Loop \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0446\u0435\u043b\u044b\u043c \u0447\u0438\u0441\u043b\u043e\u043c \u043e\u0442 1 \u0434\u043e 4.", + "loop_rfid": "RF Loop \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0431\u0435\u0437 RF Serial.", + "relay_inclusive": "\u0410\u0434\u0440\u0435\u0441 \u0440\u0435\u043b\u0435 \u0438 \u043a\u0430\u043d\u0430\u043b \u0440\u0435\u043b\u0435 \u0432\u0437\u0430\u0438\u043c\u043e\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u044b \u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0432\u043c\u0435\u0441\u0442\u0435." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u043d\u043e\u0447\u043d\u043e\u0439 \u0440\u0435\u0436\u0438\u043c", + "auto_bypass": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0432\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043f\u0440\u0438 \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0435 \u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0443", + "code_arm_required": "\u041a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0443" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c" + }, + "description": "\u0427\u0442\u043e \u0431\u044b \u0412\u044b \u0445\u043e\u0442\u0435\u043b\u0438 \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c?", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "RF Loop", + "zone_name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0437\u043e\u043d\u044b", + "zone_relayaddr": "\u0410\u0434\u0440\u0435\u0441 \u0440\u0435\u043b\u0435", + "zone_relaychan": "\u041a\u0430\u043d\u0430\u043b \u0440\u0435\u043b\u0435", + "zone_rfid": "RF Serial", + "zone_type": "\u0422\u0438\u043f \u0437\u043e\u043d\u044b" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u0437\u043e\u043d\u044b {zone_number}. \u0427\u0442\u043e\u0431\u044b \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0437\u043e\u043d\u0443 {zone_number}, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \"\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0437\u043e\u043d\u044b\" \u043f\u0443\u0441\u0442\u044b\u043c.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "\u041d\u043e\u043c\u0435\u0440 \u0437\u043e\u043d\u044b" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043d\u043e\u043c\u0435\u0440 \u0437\u043e\u043d\u044b, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c, \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u0438\u043b\u0438 \u0443\u0434\u0430\u043b\u0438\u0442\u044c.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/sl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/sl.json new file mode 100644 index 00000000000..73dfc60865e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/sl.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee nastavljena" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/sv.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/sv.json new file mode 100644 index 00000000000..6c9f0dbcb43 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "step": { + "protocol": { + "data": { + "device_path": "Enhetsv\u00e4g" + }, + "title": "Konfigurera anslutningsinst\u00e4llningar" + }, + "user": { + "data": { + "protocol": "Protokoll" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "edit_select": "Redigera" + }, + "description": "Vad vill du redigera?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/tr.json new file mode 100644 index 00000000000..276b733b31f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/tr.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "protocol": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + } + } + }, + "options": { + "error": { + "relay_inclusive": "R\u00f6le Adresi ve R\u00f6le Kanal\u0131 birbirine ba\u011fl\u0131d\u0131r ve birlikte eklenmelidir." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Alternatif Gece Modu" + } + }, + "init": { + "data": { + "edit_select": "D\u00fczenle" + } + }, + "zone_details": { + "data": { + "zone_name": "B\u00f6lge Ad\u0131", + "zone_relayaddr": "R\u00f6le Adresi", + "zone_relaychan": "R\u00f6le Kanal\u0131" + } + }, + "zone_select": { + "data": { + "zone_number": "B\u00f6lge Numaras\u0131" + }, + "title": "AlarmDecoder'\u0131 yap\u0131land\u0131r\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/uk.json new file mode 100644 index 00000000000..c19d00c0eca --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/uk.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0456\u0448\u043d\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e AlarmDecoder." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "\u0428\u0432\u0438\u0434\u043a\u0456\u0441\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0456 \u0434\u0430\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", + "device_path": "\u0428\u043b\u044f\u0445 \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + }, + "user": { + "data": { + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b AlarmDecoder" + } + } + }, + "options": { + "error": { + "int": "\u041f\u043e\u043b\u0435 \u043d\u0438\u0436\u0447\u0435 \u043c\u0430\u0454 \u0431\u0443\u0442\u0438 \u0446\u0456\u043b\u0438\u043c \u0447\u0438\u0441\u043b\u043e\u043c.", + "loop_range": "RF Loop \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0446\u0456\u043b\u0438\u043c \u0447\u0438\u0441\u043b\u043e\u043c \u0432\u0456\u0434 1 \u0434\u043e 4.", + "loop_rfid": "RF Loop \u043d\u0435 \u043c\u043e\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0431\u0435\u0437 RF Serial.", + "relay_inclusive": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0440\u0435\u043b\u0435 \u0456 \u043a\u0430\u043d\u0430\u043b \u0440\u0435\u043b\u0435 \u0432\u0437\u0430\u0454\u043c\u043e\u0437\u0430\u043b\u0435\u0436\u043d\u0456 \u0456 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0431\u0443\u0442\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0456 \u0440\u0430\u0437\u043e\u043c." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u043d\u0456\u0447\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c", + "auto_bypass": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0438\u0439 \u0432\u043a\u043b\u044e\u0447\u0430\u0442\u0438 \u0432\u0438\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438 \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u0446\u0456 \u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0443", + "code_arm_required": "\u041a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0438\u0439 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0443" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "\u0417\u043c\u0456\u043d\u0438\u0442\u0438" + }, + "description": "\u0429\u043e \u0431 \u0412\u0438 \u0445\u043e\u0442\u0456\u043b\u0438 \u0440\u0435\u0434\u0430\u0433\u0443\u0432\u0430\u0442\u0438?", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "RF Loop", + "zone_name": "\u041d\u0430\u0437\u0432\u0430 \u0437\u043e\u043d\u0438", + "zone_relayaddr": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0440\u0435\u043b\u0435", + "zone_relaychan": "\u041a\u0430\u043d\u0430\u043b \u0440\u0435\u043b\u0435", + "zone_rfid": "RF Serial", + "zone_type": "\u0422\u0438\u043f \u0437\u043e\u043d\u0438" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0434\u0430\u043d\u0456 \u0434\u043b\u044f \u0437\u043e\u043d\u0438 {zone_number}. \u0429\u043e\u0431 \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u0437\u043e\u043d\u0443 {zone_number}, \u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u043b\u0435 \"\u041d\u0430\u0437\u0432\u0430 \u0437\u043e\u043d\u0438\" \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "\u041d\u043e\u043c\u0435\u0440 \u0437\u043e\u043d\u0438" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043d\u043e\u043c\u0435\u0440 \u0437\u043e\u043d\u0438, \u044f\u043a\u0443 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438, \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u0430\u0431\u043e \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/zh-Hans.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/zh-Hans.json new file mode 100644 index 00000000000..1810fa0bcf0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/zh-Hans.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + }, + "step": { + "protocol": { + "data": { + "port": "\u7aef\u53e3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/zh-Hant.json new file mode 100644 index 00000000000..d1a96eedd15 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alarmdecoder/translations/zh-Hant.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "create_entry": { + "default": "\u6210\u529f\u9023\u7dda\u81f3 AlarmDecoder\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "\u88dd\u7f6e\u901a\u8a0a\u7387", + "device_path": "\u88dd\u7f6e\u8def\u5f91", + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + }, + "title": "\u8a2d\u5b9a\u9023\u7dda\u8a2d\u5b9a" + }, + "user": { + "data": { + "protocol": "\u901a\u8a0a\u5354\u5b9a" + }, + "title": "\u9078\u64c7 AlarmDecoder \u901a\u8a0a\u5354\u5b9a" + } + } + }, + "options": { + "error": { + "int": "\u4e0b\u65b9\u6b04\u4f4d\u5fc5\u9808\u70ba\u6574\u6578\u3002", + "loop_range": "RF \u8ff4\u8def\u5fc5\u9808\u70ba\u4ecb\u65bc 1 \u81f3 4 \u9593\u7684\u6574\u6578\u3002", + "loop_rfid": "\u5982\u679c\u6c92\u6709 RF \u5e8f\u5217\u5247\u7121\u6cd5\u4f7f\u7528 RF \u8ff4\u8def\u3002", + "relay_inclusive": "\u4e2d\u7e7c\u5730\u5740\u8207\u4e2d\u7e7c\u983b\u9053\u70ba\u76f8\u4e92\u4f9d\u8cf4\uff0c\u4e26\u5fc5\u9808\u4e00\u8d77\u5305\u542b\u3002" + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "\u66ff\u4ee3\u591c\u9593\u6a21\u5f0f", + "auto_bypass": "\u81ea\u52d5\u5ffd\u7565\u8b66\u6212", + "code_arm_required": "\u8b66\u6212\u9700\u8981\u4ee3\u78bc" + }, + "title": "\u8a2d\u5b9a AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "\u7de8\u8f2f" + }, + "description": "\u662f\u5426\u8981\u9032\u884c\u7de8\u8f2f\uff1f", + "title": "\u8a2d\u5b9a AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "RF \u8ff4\u8def", + "zone_name": "\u5340\u57df\u540d\u7a31", + "zone_relayaddr": "\u4e2d\u7e7c\u4f4d\u5740", + "zone_relaychan": "\u4e2d\u7e7c\u983b\u9053", + "zone_rfid": "RF \u5e8f\u5217", + "zone_type": "\u5340\u57df\u985e\u578b" + }, + "description": "\u8f38\u5165\u5340\u57df {zone_number} \u8a73\u7d30\u8cc7\u6599\u3002\u6b32\u522a\u9664\u5340\u57df {zone_number}\uff0c\u4fdd\u6301\u5340\u57df\u540d\u7a31\u7a7a\u767d\u3002", + "title": "\u8a2d\u5b9a AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "\u5340\u57df\u78bc" + }, + "description": "\u8f38\u5165\u6240\u8981\u65b0\u589e\u3001\u7de8\u8f2f\u6216\u79fb\u9664\u7684\u5340\u57df\u78bc\u3002", + "title": "\u8a2d\u5b9a AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alert/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alert/__init__.py new file mode 100644 index 00000000000..73bea193394 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alert/__init__.py @@ -0,0 +1,331 @@ +"""Support for repeating alerts when conditions are met.""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_MESSAGE, + ATTR_TITLE, + DOMAIN as DOMAIN_NOTIFY, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_ENTITY_ID, + CONF_NAME, + CONF_REPEAT, + CONF_STATE, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_IDLE, + STATE_OFF, + STATE_ON, +) +from homeassistant.helpers import event, service +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.util.dt import now + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "alert" + +CONF_CAN_ACK = "can_acknowledge" +CONF_NOTIFIERS = "notifiers" +CONF_SKIP_FIRST = "skip_first" +CONF_ALERT_MESSAGE = "message" +CONF_DONE_MESSAGE = "done_message" +CONF_TITLE = "title" +CONF_DATA = "data" + +DEFAULT_CAN_ACK = True +DEFAULT_SKIP_FIRST = False + +ALERT_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_STATE, default=STATE_ON): cv.string, + vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]), + vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean, + vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean, + vol.Optional(CONF_ALERT_MESSAGE): cv.template, + vol.Optional(CONF_DONE_MESSAGE): cv.template, + vol.Optional(CONF_TITLE): cv.template, + vol.Optional(CONF_DATA): dict, + vol.Required(CONF_NOTIFIERS): cv.ensure_list, + } +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: cv.schema_with_slug_keys(ALERT_SCHEMA)}, extra=vol.ALLOW_EXTRA +) + +ALERT_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_ids}) + + +def is_on(hass, entity_id): + """Return if the alert is firing and not acknowledged.""" + return hass.states.is_state(entity_id, STATE_ON) + + +async def async_setup(hass, config): + """Set up the Alert component.""" + entities = [] + + for object_id, cfg in config[DOMAIN].items(): + if not cfg: + cfg = {} + + name = cfg[CONF_NAME] + watched_entity_id = cfg[CONF_ENTITY_ID] + alert_state = cfg[CONF_STATE] + repeat = cfg[CONF_REPEAT] + skip_first = cfg[CONF_SKIP_FIRST] + message_template = cfg.get(CONF_ALERT_MESSAGE) + done_message_template = cfg.get(CONF_DONE_MESSAGE) + notifiers = cfg[CONF_NOTIFIERS] + can_ack = cfg[CONF_CAN_ACK] + title_template = cfg.get(CONF_TITLE) + data = cfg.get(CONF_DATA) + + entities.append( + Alert( + hass, + object_id, + name, + watched_entity_id, + alert_state, + repeat, + skip_first, + message_template, + done_message_template, + notifiers, + can_ack, + title_template, + data, + ) + ) + + if not entities: + return False + + async def async_handle_alert_service(service_call): + """Handle calls to alert services.""" + alert_ids = await service.async_extract_entity_ids(hass, service_call) + + for alert_id in alert_ids: + for alert in entities: + if alert.entity_id != alert_id: + continue + + alert.async_set_context(service_call.context) + if service_call.service == SERVICE_TURN_ON: + await alert.async_turn_on() + elif service_call.service == SERVICE_TOGGLE: + await alert.async_toggle() + else: + await alert.async_turn_off() + + # Setup service calls + hass.services.async_register( + DOMAIN, + SERVICE_TURN_OFF, + async_handle_alert_service, + schema=ALERT_SERVICE_SCHEMA, + ) + hass.services.async_register( + DOMAIN, SERVICE_TURN_ON, async_handle_alert_service, schema=ALERT_SERVICE_SCHEMA + ) + hass.services.async_register( + DOMAIN, SERVICE_TOGGLE, async_handle_alert_service, schema=ALERT_SERVICE_SCHEMA + ) + + for alert in entities: + alert.async_write_ha_state() + + return True + + +class Alert(ToggleEntity): + """Representation of an alert.""" + + def __init__( + self, + hass, + entity_id, + name, + watched_entity_id, + state, + repeat, + skip_first, + message_template, + done_message_template, + notifiers, + can_ack, + title_template, + data, + ): + """Initialize the alert.""" + self.hass = hass + self._name = name + self._alert_state = state + self._skip_first = skip_first + self._data = data + + self._message_template = message_template + if self._message_template is not None: + self._message_template.hass = hass + + self._done_message_template = done_message_template + if self._done_message_template is not None: + self._done_message_template.hass = hass + + self._title_template = title_template + if self._title_template is not None: + self._title_template.hass = hass + + self._notifiers = notifiers + self._can_ack = can_ack + + self._delay = [timedelta(minutes=val) for val in repeat] + self._next_delay = 0 + + self._firing = False + self._ack = False + self._cancel = None + self._send_done_message = False + self.entity_id = f"{DOMAIN}.{entity_id}" + + event.async_track_state_change_event( + hass, [watched_entity_id], self.watched_entity_change + ) + + @property + def name(self): + """Return the name of the alert.""" + return self._name + + @property + def should_poll(self): + """Home Assistant need not poll these entities.""" + return False + + @property + def state(self): + """Return the alert status.""" + if self._firing: + if self._ack: + return STATE_OFF + return STATE_ON + return STATE_IDLE + + async def watched_entity_change(self, ev): + """Determine if the alert should start or stop.""" + to_state = ev.data.get("new_state") + if to_state is None: + return + _LOGGER.debug("Watched entity (%s) has changed", ev.data.get("entity_id")) + if to_state.state == self._alert_state and not self._firing: + await self.begin_alerting() + if to_state.state != self._alert_state and self._firing: + await self.end_alerting() + + async def begin_alerting(self): + """Begin the alert procedures.""" + _LOGGER.debug("Beginning Alert: %s", self._name) + self._ack = False + self._firing = True + self._next_delay = 0 + + if not self._skip_first: + await self._notify() + else: + await self._schedule_notify() + + self.async_write_ha_state() + + async def end_alerting(self): + """End the alert procedures.""" + _LOGGER.debug("Ending Alert: %s", self._name) + self._cancel() + self._ack = False + self._firing = False + if self._send_done_message: + await self._notify_done_message() + self.async_write_ha_state() + + async def _schedule_notify(self): + """Schedule a notification.""" + delay = self._delay[self._next_delay] + next_msg = now() + delay + self._cancel = event.async_track_point_in_time( + self.hass, self._notify, next_msg + ) + self._next_delay = min(self._next_delay + 1, len(self._delay) - 1) + + async def _notify(self, *args): + """Send the alert notification.""" + if not self._firing: + return + + if not self._ack: + _LOGGER.info("Alerting: %s", self._name) + self._send_done_message = True + + if self._message_template is not None: + message = self._message_template.async_render(parse_result=False) + else: + message = self._name + + await self._send_notification_message(message) + await self._schedule_notify() + + async def _notify_done_message(self, *args): + """Send notification of complete alert.""" + _LOGGER.info("Alerting: %s", self._done_message_template) + self._send_done_message = False + + if self._done_message_template is None: + return + + message = self._done_message_template.async_render(parse_result=False) + + await self._send_notification_message(message) + + async def _send_notification_message(self, message): + + msg_payload = {ATTR_MESSAGE: message} + + if self._title_template is not None: + title = self._title_template.async_render(parse_result=False) + msg_payload.update({ATTR_TITLE: title}) + if self._data: + msg_payload.update({ATTR_DATA: self._data}) + + _LOGGER.debug(msg_payload) + + for target in self._notifiers: + await self.hass.services.async_call( + DOMAIN_NOTIFY, target, msg_payload, context=self._context + ) + + async def async_turn_on(self, **kwargs): + """Async Unacknowledge alert.""" + _LOGGER.debug("Reset Alert: %s", self._name) + self._ack = False + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs): + """Async Acknowledge alert.""" + _LOGGER.debug("Acknowledged Alert: %s", self._name) + self._ack = True + self.async_write_ha_state() + + async def async_toggle(self, **kwargs): + """Async toggle alert.""" + if self._ack: + return await self.async_turn_on() + return await self.async_turn_off() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alert/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alert/manifest.json new file mode 100644 index 00000000000..f5d3e08f2fe --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alert/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "alert", + "name": "Alert", + "documentation": "https://www.home-assistant.io/integrations/alert", + "after_dependencies": ["notify"], + "codeowners": [], + "quality_scale": "internal", + "iot_class": "local_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alert/reproduce_state.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alert/reproduce_state.py new file mode 100644 index 00000000000..9c8cbd19810 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alert/reproduce_state.py @@ -0,0 +1,78 @@ +"""Reproduce an Alert state.""" +from __future__ import annotations + +import asyncio +from collections.abc import Iterable +import logging +from typing import Any + +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import Context, HomeAssistant, State + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATE_ON, STATE_OFF} + + +async def _async_reproduce_state( + hass: HomeAssistant, + state: State, + *, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATE_ON: + service = SERVICE_TURN_ON + + elif state.state == STATE_OFF: + service = SERVICE_TURN_OFF + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistant, + states: Iterable[State], + *, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, +) -> None: + """Reproduce Alert states.""" + # Reproduce states in parallel. + await asyncio.gather( + *( + _async_reproduce_state( + hass, state, context=context, reproduce_options=reproduce_options + ) + for state in states + ) + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alert/services.yaml b/homeassistant-2021.6.0.dev0/homeassistant/components/alert/services.yaml new file mode 100644 index 00000000000..3242a9cedb4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alert/services.yaml @@ -0,0 +1,20 @@ +toggle: + name: Toggle + description: Toggle alert's notifications. + target: + entity: + domain: alert + +turn_off: + name: Turn off + description: Silence alert's notifications. + target: + entity: + domain: alert + +turn_on: + name: Turn on + description: Reset alert's notifications. + target: + entity: + domain: alert diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/__init__.py new file mode 100644 index 00000000000..d388a22983f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/__init__.py @@ -0,0 +1,103 @@ +"""Support for Alexa skill service end point.""" +import voluptuous as vol + +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_DESCRIPTION, + CONF_NAME, + CONF_PASSWORD, +) +from homeassistant.helpers import config_validation as cv, entityfilter + +from . import flash_briefings, intent, smart_home_http +from .const import ( + CONF_AUDIO, + CONF_DISPLAY_CATEGORIES, + CONF_DISPLAY_URL, + CONF_ENDPOINT, + CONF_ENTITY_CONFIG, + CONF_FILTER, + CONF_LOCALE, + CONF_SUPPORTED_LOCALES, + CONF_TEXT, + CONF_TITLE, + CONF_UID, + DOMAIN, +) + +CONF_FLASH_BRIEFINGS = "flash_briefings" +CONF_SMART_HOME = "smart_home" +DEFAULT_LOCALE = "en-US" + +ALEXA_ENTITY_SCHEMA = vol.Schema( + { + vol.Optional(CONF_DESCRIPTION): cv.string, + vol.Optional(CONF_DISPLAY_CATEGORIES): cv.string, + vol.Optional(CONF_NAME): cv.string, + } +) + +SMART_HOME_SCHEMA = vol.Schema( + { + vol.Optional(CONF_ENDPOINT): cv.string, + vol.Optional(CONF_CLIENT_ID): cv.string, + vol.Optional(CONF_CLIENT_SECRET): cv.string, + vol.Optional(CONF_LOCALE, default=DEFAULT_LOCALE): vol.In( + CONF_SUPPORTED_LOCALES + ), + vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA, + vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA}, + } +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: { + CONF_FLASH_BRIEFINGS: { + vol.Required(CONF_PASSWORD): cv.string, + cv.string: vol.All( + cv.ensure_list, + [ + { + vol.Optional(CONF_UID): cv.string, + vol.Required(CONF_TITLE): cv.template, + vol.Optional(CONF_AUDIO): cv.template, + vol.Required(CONF_TEXT, default=""): cv.template, + vol.Optional(CONF_DISPLAY_URL): cv.template, + } + ], + ), + }, + # vol.Optional here would mean we couldn't distinguish between an empty + # smart_home: and none at all. + CONF_SMART_HOME: vol.Any(SMART_HOME_SCHEMA, None), + } + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Activate the Alexa component.""" + if DOMAIN not in config: + return True + + config = config[DOMAIN] + + flash_briefings_config = config.get(CONF_FLASH_BRIEFINGS) + + intent.async_setup(hass) + + if flash_briefings_config: + flash_briefings.async_setup(hass, flash_briefings_config) + + try: + smart_home_config = config[CONF_SMART_HOME] + except KeyError: + pass + else: + smart_home_config = smart_home_config or SMART_HOME_SCHEMA({}) + await smart_home_http.async_setup(hass, smart_home_config) + + return True diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/auth.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/auth.py new file mode 100644 index 00000000000..433b2929602 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/auth.py @@ -0,0 +1,162 @@ +"""Support for Alexa skill auth.""" +import asyncio +from datetime import timedelta +import json +import logging + +import aiohttp +import async_timeout + +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, HTTP_OK +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client +from homeassistant.util import dt + +_LOGGER = logging.getLogger(__name__) + +LWA_TOKEN_URI = "https://api.amazon.com/auth/o2/token" +LWA_HEADERS = {"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"} + +PREEMPTIVE_REFRESH_TTL_IN_SECONDS = 300 +STORAGE_KEY = "alexa_auth" +STORAGE_VERSION = 1 +STORAGE_EXPIRE_TIME = "expire_time" +STORAGE_ACCESS_TOKEN = "access_token" +STORAGE_REFRESH_TOKEN = "refresh_token" + + +class Auth: + """Handle authentication to send events to Alexa.""" + + def __init__(self, hass, client_id, client_secret): + """Initialize the Auth class.""" + self.hass = hass + + self.client_id = client_id + self.client_secret = client_secret + + self._prefs = None + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + + self._get_token_lock = asyncio.Lock() + + async def async_do_auth(self, accept_grant_code): + """Do authentication with an AcceptGrant code.""" + # access token not retrieved yet for the first time, so this should + # be an access token request + + lwa_params = { + "grant_type": "authorization_code", + "code": accept_grant_code, + CONF_CLIENT_ID: self.client_id, + CONF_CLIENT_SECRET: self.client_secret, + } + _LOGGER.debug( + "Calling LWA to get the access token (first time), with: %s", + json.dumps(lwa_params), + ) + + return await self._async_request_new_token(lwa_params) + + @callback + def async_invalidate_access_token(self): + """Invalidate access token.""" + self._prefs[STORAGE_ACCESS_TOKEN] = None + + async def async_get_access_token(self): + """Perform access token or token refresh request.""" + async with self._get_token_lock: + if self._prefs is None: + await self.async_load_preferences() + + if self.is_token_valid(): + _LOGGER.debug("Token still valid, using it") + return self._prefs[STORAGE_ACCESS_TOKEN] + + if self._prefs[STORAGE_REFRESH_TOKEN] is None: + _LOGGER.debug("Token invalid and no refresh token available") + return None + + lwa_params = { + "grant_type": "refresh_token", + "refresh_token": self._prefs[STORAGE_REFRESH_TOKEN], + CONF_CLIENT_ID: self.client_id, + CONF_CLIENT_SECRET: self.client_secret, + } + + _LOGGER.debug("Calling LWA to refresh the access token") + return await self._async_request_new_token(lwa_params) + + @callback + def is_token_valid(self): + """Check if a token is already loaded and if it is still valid.""" + if not self._prefs[STORAGE_ACCESS_TOKEN]: + return False + + expire_time = dt.parse_datetime(self._prefs[STORAGE_EXPIRE_TIME]) + preemptive_expire_time = expire_time - timedelta( + seconds=PREEMPTIVE_REFRESH_TTL_IN_SECONDS + ) + + return dt.utcnow() < preemptive_expire_time + + async def _async_request_new_token(self, lwa_params): + + try: + session = aiohttp_client.async_get_clientsession(self.hass) + with async_timeout.timeout(10): + response = await session.post( + LWA_TOKEN_URI, + headers=LWA_HEADERS, + data=lwa_params, + allow_redirects=True, + ) + + except (asyncio.TimeoutError, aiohttp.ClientError): + _LOGGER.error("Timeout calling LWA to get auth token") + return None + + _LOGGER.debug("LWA response header: %s", response.headers) + _LOGGER.debug("LWA response status: %s", response.status) + + if response.status != HTTP_OK: + _LOGGER.error("Error calling LWA to get auth token") + return None + + response_json = await response.json() + _LOGGER.debug("LWA response body : %s", response_json) + + access_token = response_json["access_token"] + refresh_token = response_json["refresh_token"] + expires_in = response_json["expires_in"] + expire_time = dt.utcnow() + timedelta(seconds=expires_in) + + await self._async_update_preferences( + access_token, refresh_token, expire_time.isoformat() + ) + + return access_token + + async def async_load_preferences(self): + """Load preferences with stored tokens.""" + self._prefs = await self._store.async_load() + + if self._prefs is None: + self._prefs = { + STORAGE_ACCESS_TOKEN: None, + STORAGE_REFRESH_TOKEN: None, + STORAGE_EXPIRE_TIME: None, + } + + async def _async_update_preferences(self, access_token, refresh_token, expire_time): + """Update user preferences.""" + if self._prefs is None: + await self.async_load_preferences() + + if access_token is not None: + self._prefs[STORAGE_ACCESS_TOKEN] = access_token + if refresh_token is not None: + self._prefs[STORAGE_REFRESH_TOKEN] = refresh_token + if expire_time is not None: + self._prefs[STORAGE_EXPIRE_TIME] = expire_time + await self._store.async_save(self._prefs) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/capabilities.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/capabilities.py new file mode 100644 index 00000000000..1afe65b7bc6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/capabilities.py @@ -0,0 +1,2023 @@ +"""Alexa capabilities.""" +from __future__ import annotations + +import logging + +from homeassistant.components import ( + cover, + fan, + image_processing, + input_number, + light, + timer, + vacuum, +) +from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) +import homeassistant.components.climate.const as climate +import homeassistant.components.media_player.const as media_player +from homeassistant.const import ( + ATTR_SUPPORTED_FEATURES, + ATTR_TEMPERATURE, + ATTR_UNIT_OF_MEASUREMENT, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_IDLE, + STATE_LOCKED, + STATE_OFF, + STATE_ON, + STATE_PAUSED, + STATE_PLAYING, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + STATE_UNLOCKED, +) +from homeassistant.core import State +import homeassistant.util.color as color_util +import homeassistant.util.dt as dt_util + +from .const import ( + API_TEMP_UNITS, + API_THERMOSTAT_MODES, + API_THERMOSTAT_PRESETS, + DATE_FORMAT, + Inputs, +) +from .errors import UnsupportedProperty +from .resources import ( + AlexaCapabilityResource, + AlexaGlobalCatalog, + AlexaModeResource, + AlexaPresetResource, + AlexaSemantics, +) + +_LOGGER = logging.getLogger(__name__) + + +class AlexaCapability: + """Base class for Alexa capability interfaces. + + The Smart Home Skills API defines a number of "capability interfaces", + roughly analogous to domains in Home Assistant. The supported interfaces + describe what actions can be performed on a particular device. + + https://developer.amazon.com/docs/device-apis/message-guide.html + """ + + supported_locales = {"en-US"} + + def __init__(self, entity: State, instance: str | None = None): + """Initialize an Alexa capability.""" + self.entity = entity + self.instance = instance + + def name(self) -> str: + """Return the Alexa API name of this interface.""" + raise NotImplementedError + + @staticmethod + def properties_supported() -> list[dict]: + """Return what properties this entity supports.""" + return [] + + @staticmethod + def properties_proactively_reported() -> bool: + """Return True if properties asynchronously reported.""" + return False + + @staticmethod + def properties_retrievable() -> bool: + """Return True if properties can be retrieved.""" + return False + + @staticmethod + def properties_non_controllable() -> bool: + """Return True if non controllable.""" + return None + + @staticmethod + def get_property(name): + """Read and return a property. + + Return value should be a dict, or raise UnsupportedProperty. + + Properties can also have a timeOfSample and uncertaintyInMilliseconds, + but returning those metadata is not yet implemented. + """ + raise UnsupportedProperty(name) + + @staticmethod + def supports_deactivation(): + """Applicable only to scenes.""" + return None + + @staticmethod + def capability_proactively_reported(): + """Return True if the capability is proactively reported. + + Set properties_proactively_reported() for proactively reported properties. + Applicable to DoorbellEventSource. + """ + return None + + @staticmethod + def capability_resources(): + """Return the capability object. + + Applicable to ToggleController, RangeController, and ModeController interfaces. + """ + return [] + + @staticmethod + def configuration(): + """Return the configuration object. + + Applicable to the ThermostatController, SecurityControlPanel, ModeController, RangeController, + and EventDetectionSensor. + """ + return [] + + @staticmethod + def configurations(): + """Return the configurations object. + + The plural configurations object is different that the singular configuration object. + Applicable to EqualizerController interface. + """ + return [] + + @staticmethod + def inputs(): + """Applicable only to media players.""" + return [] + + @staticmethod + def semantics(): + """Return the semantics object. + + Applicable to ToggleController, RangeController, and ModeController interfaces. + """ + return [] + + @staticmethod + def supported_operations(): + """Return the supportedOperations object.""" + return [] + + @staticmethod + def camera_stream_configurations(): + """Applicable only to CameraStreamController.""" + return None + + def serialize_discovery(self): + """Serialize according to the Discovery API.""" + result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"} + + instance = self.instance + if instance is not None: + result["instance"] = instance + + properties_supported = self.properties_supported() + if properties_supported: + result["properties"] = { + "supported": self.properties_supported(), + "proactivelyReported": self.properties_proactively_reported(), + "retrievable": self.properties_retrievable(), + } + + proactively_reported = self.capability_proactively_reported() + if proactively_reported is not None: + result["proactivelyReported"] = proactively_reported + + non_controllable = self.properties_non_controllable() + if non_controllable is not None: + result["properties"]["nonControllable"] = non_controllable + + supports_deactivation = self.supports_deactivation() + if supports_deactivation is not None: + result["supportsDeactivation"] = supports_deactivation + + capability_resources = self.capability_resources() + if capability_resources: + result["capabilityResources"] = capability_resources + + configuration = self.configuration() + if configuration: + result["configuration"] = configuration + + # The plural configurations object is different than the singular configuration object above. + configurations = self.configurations() + if configurations: + result["configurations"] = configurations + + semantics = self.semantics() + if semantics: + result["semantics"] = semantics + + supported_operations = self.supported_operations() + if supported_operations: + result["supportedOperations"] = supported_operations + + inputs = self.inputs() + if inputs: + result["inputs"] = inputs + + camera_stream_configurations = self.camera_stream_configurations() + if camera_stream_configurations: + result["cameraStreamConfigurations"] = camera_stream_configurations + + return result + + def serialize_properties(self): + """Return properties serialized for an API response.""" + for prop in self.properties_supported(): + prop_name = prop["name"] + try: + prop_value = self.get_property(prop_name) + except UnsupportedProperty: + raise + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Unexpected error getting %s.%s property from %s", + self.name(), + prop_name, + self.entity, + ) + prop_value = None + + if prop_value is None: + continue + + result = { + "name": prop_name, + "namespace": self.name(), + "value": prop_value, + "timeOfSample": dt_util.utcnow().strftime(DATE_FORMAT), + "uncertaintyInMilliseconds": 0, + } + instance = self.instance + if instance is not None: + result["instance"] = instance + + yield result + + +class Alexa(AlexaCapability): + """Implements Alexa Interface. + + Although endpoints implement this interface implicitly, + The API suggests you should explicitly include this interface. + + https://developer.amazon.com/docs/device-apis/alexa-interface.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "fr-CA", + "fr-FR", + "it-IT", + "ja-JP", + } + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa" + + +class AlexaEndpointHealth(AlexaCapability): + """Implements Alexa.EndpointHealth. + + https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-when-alexa-requests-it + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fr-FR", + "it-IT", + "ja-JP", + } + + def __init__(self, hass, entity): + """Initialize the entity.""" + super().__init__(entity) + self.hass = hass + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.EndpointHealth" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "connectivity"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "connectivity": + raise UnsupportedProperty(name) + + if self.entity.state == STATE_UNAVAILABLE: + return {"value": "UNREACHABLE"} + return {"value": "OK"} + + +class AlexaPowerController(AlexaCapability): + """Implements Alexa.PowerController. + + https://developer.amazon.com/docs/device-apis/alexa-powercontroller.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fr-FR", + "it-IT", + "ja-JP", + } + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.PowerController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "powerState"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "powerState": + raise UnsupportedProperty(name) + + if self.entity.domain == climate.DOMAIN: + is_on = self.entity.state != climate.HVAC_MODE_OFF + elif self.entity.domain == vacuum.DOMAIN: + is_on = self.entity.state == vacuum.STATE_CLEANING + elif self.entity.domain == timer.DOMAIN: + is_on = self.entity.state != STATE_IDLE + + else: + is_on = self.entity.state != STATE_OFF + + return "ON" if is_on else "OFF" + + +class AlexaLockController(AlexaCapability): + """Implements Alexa.LockController. + + https://developer.amazon.com/docs/device-apis/alexa-lockcontroller.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "es-US", + "fr-CA", + "fr-FR", + "hi-IN", + "it-IT", + "ja-JP", + "pt-BR", + } + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.LockController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "lockState"}] + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "lockState": + raise UnsupportedProperty(name) + + if self.entity.state == STATE_LOCKED: + return "LOCKED" + if self.entity.state == STATE_UNLOCKED: + return "UNLOCKED" + return "JAMMED" + + +class AlexaSceneController(AlexaCapability): + """Implements Alexa.SceneController. + + https://developer.amazon.com/docs/device-apis/alexa-scenecontroller.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fr-FR", + "it-IT", + "ja-JP", + } + + def __init__(self, entity, supports_deactivation): + """Initialize the entity.""" + super().__init__(entity) + self.supports_deactivation = lambda: supports_deactivation + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.SceneController" + + +class AlexaBrightnessController(AlexaCapability): + """Implements Alexa.BrightnessController. + + https://developer.amazon.com/docs/device-apis/alexa-brightnesscontroller.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fr-FR", + "hi-IN", + "it-IT", + "ja-JP", + "pt-BR", + } + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.BrightnessController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "brightness"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "brightness": + raise UnsupportedProperty(name) + if "brightness" in self.entity.attributes: + return round(self.entity.attributes["brightness"] / 255.0 * 100) + return 0 + + +class AlexaColorController(AlexaCapability): + """Implements Alexa.ColorController. + + https://developer.amazon.com/docs/device-apis/alexa-colorcontroller.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fr-FR", + "hi-IN", + "it-IT", + "ja-JP", + "pt-BR", + } + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.ColorController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "color"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "color": + raise UnsupportedProperty(name) + + hue, saturation = self.entity.attributes.get(light.ATTR_HS_COLOR, (0, 0)) + + return { + "hue": hue, + "saturation": saturation / 100.0, + "brightness": self.entity.attributes.get(light.ATTR_BRIGHTNESS, 0) / 255.0, + } + + +class AlexaColorTemperatureController(AlexaCapability): + """Implements Alexa.ColorTemperatureController. + + https://developer.amazon.com/docs/device-apis/alexa-colortemperaturecontroller.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fr-FR", + "hi-IN", + "it-IT", + "ja-JP", + "pt-BR", + } + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.ColorTemperatureController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "colorTemperatureInKelvin"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "colorTemperatureInKelvin": + raise UnsupportedProperty(name) + if "color_temp" in self.entity.attributes: + return color_util.color_temperature_mired_to_kelvin( + self.entity.attributes["color_temp"] + ) + return None + + +class AlexaPercentageController(AlexaCapability): + """Implements Alexa.PercentageController. + + https://developer.amazon.com/docs/device-apis/alexa-percentagecontroller.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fr-FR", + "it-IT", + "ja-JP", + } + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.PercentageController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "percentage"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "percentage": + raise UnsupportedProperty(name) + + if self.entity.domain == fan.DOMAIN: + return self.entity.attributes.get(fan.ATTR_PERCENTAGE) or 0 + + if self.entity.domain == cover.DOMAIN: + return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION, 0) + + return 0 + + +class AlexaSpeaker(AlexaCapability): + """Implements Alexa.Speaker. + + https://developer.amazon.com/docs/device-apis/alexa-speaker.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "it-IT", + "ja-JP", + } + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.Speaker" + + def properties_supported(self): + """Return what properties this entity supports.""" + properties = [{"name": "volume"}] + + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if supported & media_player.SUPPORT_VOLUME_MUTE: + properties.append({"name": "muted"}) + + return properties + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name == "volume": + current_level = self.entity.attributes.get( + media_player.ATTR_MEDIA_VOLUME_LEVEL + ) + if current_level is not None: + return round(float(current_level) * 100) + + if name == "muted": + return bool( + self.entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_MUTED) + ) + + return None + + +class AlexaStepSpeaker(AlexaCapability): + """Implements Alexa.StepSpeaker. + + https://developer.amazon.com/docs/device-apis/alexa-stepspeaker.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "it-IT", + } + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.StepSpeaker" + + +class AlexaPlaybackController(AlexaCapability): + """Implements Alexa.PlaybackController. + + https://developer.amazon.com/docs/device-apis/alexa-playbackcontroller.html + """ + + supported_locales = {"de-DE", "en-AU", "en-CA", "en-GB", "en-IN", "en-US", "fr-FR"} + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.PlaybackController" + + def supported_operations(self): + """Return the supportedOperations object. + + Supported Operations: FastForward, Next, Pause, Play, Previous, Rewind, StartOver, Stop + """ + supported_features = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + operations = { + media_player.SUPPORT_NEXT_TRACK: "Next", + media_player.SUPPORT_PAUSE: "Pause", + media_player.SUPPORT_PLAY: "Play", + media_player.SUPPORT_PREVIOUS_TRACK: "Previous", + media_player.SUPPORT_STOP: "Stop", + } + + return [ + value + for operation, value in operations.items() + if operation & supported_features + ] + + +class AlexaInputController(AlexaCapability): + """Implements Alexa.InputController. + + https://developer.amazon.com/docs/device-apis/alexa-inputcontroller.html + """ + + supported_locales = {"de-DE", "en-AU", "en-CA", "en-GB", "en-IN", "en-US"} + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.InputController" + + def inputs(self): + """Return the list of valid supported inputs.""" + source_list = self.entity.attributes.get( + media_player.ATTR_INPUT_SOURCE_LIST, [] + ) + return AlexaInputController.get_valid_inputs(source_list) + + @staticmethod + def get_valid_inputs(source_list): + """Return list of supported inputs.""" + input_list = [] + for source in source_list: + formatted_source = ( + source.lower().replace("-", "").replace("_", "").replace(" ", "") + ) + if formatted_source in Inputs.VALID_SOURCE_NAME_MAP: + input_list.append( + {"name": Inputs.VALID_SOURCE_NAME_MAP[formatted_source]} + ) + + return input_list + + +class AlexaTemperatureSensor(AlexaCapability): + """Implements Alexa.TemperatureSensor. + + https://developer.amazon.com/docs/device-apis/alexa-temperaturesensor.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fr-FR", + "it-IT", + "ja-JP", + } + + def __init__(self, hass, entity): + """Initialize the entity.""" + super().__init__(entity) + self.hass = hass + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.TemperatureSensor" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "temperature"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "temperature": + raise UnsupportedProperty(name) + + unit = self.entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + temp = self.entity.state + if self.entity.domain == climate.DOMAIN: + unit = self.hass.config.units.temperature_unit + temp = self.entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE) + + if temp in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): + return None + + try: + temp = float(temp) + except ValueError: + _LOGGER.warning("Invalid temp value %s for %s", temp, self.entity.entity_id) + return None + + return {"value": temp, "scale": API_TEMP_UNITS[unit]} + + +class AlexaContactSensor(AlexaCapability): + """Implements Alexa.ContactSensor. + + The Alexa.ContactSensor interface describes the properties and events used + to report the state of an endpoint that detects contact between two + surfaces. For example, a contact sensor can report whether a door or window + is open. + + https://developer.amazon.com/docs/device-apis/alexa-contactsensor.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-IN", + "en-US", + "es-ES", + "it-IT", + "ja-JP", + } + + def __init__(self, hass, entity): + """Initialize the entity.""" + super().__init__(entity) + self.hass = hass + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.ContactSensor" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "detectionState"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "detectionState": + raise UnsupportedProperty(name) + + if self.entity.state == STATE_ON: + return "DETECTED" + return "NOT_DETECTED" + + +class AlexaMotionSensor(AlexaCapability): + """Implements Alexa.MotionSensor. + + https://developer.amazon.com/docs/device-apis/alexa-motionsensor.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-IN", + "en-US", + "es-ES", + "it-IT", + "ja-JP", + "pt-BR", + } + + def __init__(self, hass, entity): + """Initialize the entity.""" + super().__init__(entity) + self.hass = hass + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.MotionSensor" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "detectionState"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "detectionState": + raise UnsupportedProperty(name) + + if self.entity.state == STATE_ON: + return "DETECTED" + return "NOT_DETECTED" + + +class AlexaThermostatController(AlexaCapability): + """Implements Alexa.ThermostatController. + + https://developer.amazon.com/docs/device-apis/alexa-thermostatcontroller.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fr-FR", + "it-IT", + "ja-JP", + "pt-BR", + } + + def __init__(self, hass, entity): + """Initialize the entity.""" + super().__init__(entity) + self.hass = hass + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.ThermostatController" + + def properties_supported(self): + """Return what properties this entity supports.""" + properties = [{"name": "thermostatMode"}] + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if supported & climate.SUPPORT_TARGET_TEMPERATURE: + properties.append({"name": "targetSetpoint"}) + if supported & climate.SUPPORT_TARGET_TEMPERATURE_RANGE: + properties.append({"name": "lowerSetpoint"}) + properties.append({"name": "upperSetpoint"}) + return properties + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if self.entity.state == STATE_UNAVAILABLE: + return None + + if name == "thermostatMode": + preset = self.entity.attributes.get(climate.ATTR_PRESET_MODE) + + if preset in API_THERMOSTAT_PRESETS: + mode = API_THERMOSTAT_PRESETS[preset] + else: + mode = API_THERMOSTAT_MODES.get(self.entity.state) + if mode is None: + _LOGGER.error( + "%s (%s) has unsupported state value '%s'", + self.entity.entity_id, + type(self.entity), + self.entity.state, + ) + raise UnsupportedProperty(name) + return mode + + unit = self.hass.config.units.temperature_unit + if name == "targetSetpoint": + temp = self.entity.attributes.get(ATTR_TEMPERATURE) + elif name == "lowerSetpoint": + temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW) + elif name == "upperSetpoint": + temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH) + else: + raise UnsupportedProperty(name) + + if temp is None: + return None + + try: + temp = float(temp) + except ValueError: + _LOGGER.warning( + "Invalid temp value %s for %s in %s", temp, name, self.entity.entity_id + ) + return None + + return {"value": temp, "scale": API_TEMP_UNITS[unit]} + + def configuration(self): + """Return configuration object. + + Translates climate HVAC_MODES and PRESETS to supported Alexa ThermostatMode Values. + ThermostatMode Value must be AUTO, COOL, HEAT, ECO, OFF, or CUSTOM. + """ + supported_modes = [] + hvac_modes = self.entity.attributes.get(climate.ATTR_HVAC_MODES) + for mode in hvac_modes: + thermostat_mode = API_THERMOSTAT_MODES.get(mode) + if thermostat_mode: + supported_modes.append(thermostat_mode) + + preset_modes = self.entity.attributes.get(climate.ATTR_PRESET_MODES) + if preset_modes: + for mode in preset_modes: + thermostat_mode = API_THERMOSTAT_PRESETS.get(mode) + if thermostat_mode: + supported_modes.append(thermostat_mode) + + # Return False for supportsScheduling until supported with event listener in handler. + configuration = {"supportsScheduling": False} + + if supported_modes: + configuration["supportedModes"] = supported_modes + + return configuration + + +class AlexaPowerLevelController(AlexaCapability): + """Implements Alexa.PowerLevelController. + + https://developer.amazon.com/docs/device-apis/alexa-powerlevelcontroller.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fr-FR", + "it-IT", + "ja-JP", + } + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.PowerLevelController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "powerLevel"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "powerLevel": + raise UnsupportedProperty(name) + + if self.entity.domain == fan.DOMAIN: + return self.entity.attributes.get(fan.ATTR_PERCENTAGE) or 0 + + return None + + +class AlexaSecurityPanelController(AlexaCapability): + """Implements Alexa.SecurityPanelController. + + https://developer.amazon.com/docs/device-apis/alexa-securitypanelcontroller.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "es-US", + "fr-CA", + "fr-FR", + "it-IT", + "ja-JP", + "pt-BR", + } + + def __init__(self, hass, entity): + """Initialize the entity.""" + super().__init__(entity) + self.hass = hass + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.SecurityPanelController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "armState"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "armState": + raise UnsupportedProperty(name) + + arm_state = self.entity.state + if arm_state == STATE_ALARM_ARMED_HOME: + return "ARMED_STAY" + if arm_state == STATE_ALARM_ARMED_AWAY: + return "ARMED_AWAY" + if arm_state == STATE_ALARM_ARMED_NIGHT: + return "ARMED_NIGHT" + if arm_state == STATE_ALARM_ARMED_CUSTOM_BYPASS: + return "ARMED_STAY" + return "DISARMED" + + def configuration(self): + """Return configuration object with supported authorization types.""" + code_format = self.entity.attributes.get(ATTR_CODE_FORMAT) + supported = self.entity.attributes[ATTR_SUPPORTED_FEATURES] + configuration = {} + + supported_arm_states = [{"value": "DISARMED"}] + if supported & SUPPORT_ALARM_ARM_AWAY: + supported_arm_states.append({"value": "ARMED_AWAY"}) + if supported & SUPPORT_ALARM_ARM_HOME: + supported_arm_states.append({"value": "ARMED_STAY"}) + if supported & SUPPORT_ALARM_ARM_NIGHT: + supported_arm_states.append({"value": "ARMED_NIGHT"}) + + configuration["supportedArmStates"] = supported_arm_states + + if code_format == FORMAT_NUMBER: + configuration["supportedAuthorizationTypes"] = [{"type": "FOUR_DIGIT_PIN"}] + + return configuration + + +class AlexaModeController(AlexaCapability): + """Implements Alexa.ModeController. + + The instance property must be unique across ModeController, RangeController, ToggleController within the same device. + The instance property should be a concatenated string of device domain period and single word. + e.g. fan.speed & fan.direction. + + The instance property must not contain words from other instance property strings within the same device. + e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail. + + An instance property string value may be reused for different devices. + + https://developer.amazon.com/docs/device-apis/alexa-modecontroller.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "fr-CA", + "fr-FR", + "it-IT", + "ja-JP", + } + + def __init__(self, entity, instance, non_controllable=False): + """Initialize the entity.""" + super().__init__(entity, instance) + self._resource = None + self._semantics = None + self.properties_non_controllable = lambda: non_controllable + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.ModeController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "mode"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "mode": + raise UnsupportedProperty(name) + + # Fan Direction + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": + mode = self.entity.attributes.get(fan.ATTR_DIRECTION, None) + if mode in (fan.DIRECTION_FORWARD, fan.DIRECTION_REVERSE, STATE_UNKNOWN): + return f"{fan.ATTR_DIRECTION}.{mode}" + + # Cover Position + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + # Return state instead of position when using ModeController. + mode = self.entity.state + if mode in ( + cover.STATE_OPEN, + cover.STATE_OPENING, + cover.STATE_CLOSED, + cover.STATE_CLOSING, + STATE_UNKNOWN, + ): + return f"{cover.ATTR_POSITION}.{mode}" + + return None + + def configuration(self): + """Return configuration with modeResources.""" + if isinstance(self._resource, AlexaCapabilityResource): + return self._resource.serialize_configuration() + + return None + + def capability_resources(self): + """Return capabilityResources object.""" + + # Fan Direction Resource + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": + self._resource = AlexaModeResource( + [AlexaGlobalCatalog.SETTING_DIRECTION], False + ) + self._resource.add_mode( + f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_FORWARD}", [fan.DIRECTION_FORWARD] + ) + self._resource.add_mode( + f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_REVERSE}", [fan.DIRECTION_REVERSE] + ) + return self._resource.serialize_capability_resources() + + # Cover Position Resources + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + self._resource = AlexaModeResource( + ["Position", AlexaGlobalCatalog.SETTING_OPENING], False + ) + self._resource.add_mode( + f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}", + [AlexaGlobalCatalog.VALUE_OPEN], + ) + self._resource.add_mode( + f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}", + [AlexaGlobalCatalog.VALUE_CLOSE], + ) + self._resource.add_mode( + f"{cover.ATTR_POSITION}.custom", + ["Custom", AlexaGlobalCatalog.SETTING_PRESET], + ) + return self._resource.serialize_capability_resources() + + return None + + def semantics(self): + """Build and return semantics object.""" + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + # Cover Position + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + lower_labels = [AlexaSemantics.ACTION_LOWER] + raise_labels = [AlexaSemantics.ACTION_RAISE] + self._semantics = AlexaSemantics() + + # Add open/close semantics if tilt is not supported. + if not supported & cover.SUPPORT_SET_TILT_POSITION: + lower_labels.append(AlexaSemantics.ACTION_CLOSE) + raise_labels.append(AlexaSemantics.ACTION_OPEN) + self._semantics.add_states_to_value( + [AlexaSemantics.STATES_CLOSED], + f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}", + ) + self._semantics.add_states_to_value( + [AlexaSemantics.STATES_OPEN], + f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}", + ) + + self._semantics.add_action_to_directive( + lower_labels, + "SetMode", + {"mode": f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}"}, + ) + self._semantics.add_action_to_directive( + raise_labels, + "SetMode", + {"mode": f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}"}, + ) + + return self._semantics.serialize_semantics() + + return None + + +class AlexaRangeController(AlexaCapability): + """Implements Alexa.RangeController. + + The instance property must be unique across ModeController, RangeController, ToggleController within the same device. + The instance property should be a concatenated string of device domain period and single word. + e.g. fan.speed & fan.direction. + + The instance property must not contain words from other instance property strings within the same device. + e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail. + + An instance property string value may be reused for different devices. + + https://developer.amazon.com/docs/device-apis/alexa-rangecontroller.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "fr-CA", + "fr-FR", + "it-IT", + "ja-JP", + } + + def __init__(self, entity, instance, non_controllable=False): + """Initialize the entity.""" + super().__init__(entity, instance) + self._resource = None + self._semantics = None + self.properties_non_controllable = lambda: non_controllable + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.RangeController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "rangeValue"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "rangeValue": + raise UnsupportedProperty(name) + + # Return None for unavailable and unknown states. + # Allows the Alexa.EndpointHealth Interface to handle the unavailable state in a stateReport. + if self.entity.state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): + return None + + # Fan Speed + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + speed_list = self.entity.attributes.get(fan.ATTR_SPEED_LIST) + speed = self.entity.attributes.get(fan.ATTR_SPEED) + if speed_list is not None and speed is not None: + speed_index = next( + (i for i, v in enumerate(speed_list) if v == speed), None + ) + return speed_index + + # Cover Position + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION) + + # Cover Tilt + if self.instance == f"{cover.DOMAIN}.tilt": + return self.entity.attributes.get(cover.ATTR_CURRENT_TILT_POSITION) + + # Input Number Value + if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": + return float(self.entity.state) + + # Vacuum Fan Speed + if self.instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}": + speed_list = self.entity.attributes.get(vacuum.ATTR_FAN_SPEED_LIST) + speed = self.entity.attributes.get(vacuum.ATTR_FAN_SPEED) + if speed_list is not None and speed is not None: + speed_index = next( + (i for i, v in enumerate(speed_list) if v == speed), None + ) + return speed_index + + return None + + def configuration(self): + """Return configuration with presetResources.""" + if isinstance(self._resource, AlexaCapabilityResource): + return self._resource.serialize_configuration() + + return None + + def capability_resources(self): + """Return capabilityResources object.""" + + # Fan Speed Resources + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + speed_list = self.entity.attributes[fan.ATTR_SPEED_LIST] + max_value = len(speed_list) - 1 + self._resource = AlexaPresetResource( + labels=[AlexaGlobalCatalog.SETTING_FAN_SPEED], + min_value=0, + max_value=max_value, + precision=1, + ) + for index, speed in enumerate(speed_list): + labels = [] + if isinstance(speed, str): + labels.append(speed.replace("_", " ")) + if index == 1: + labels.append(AlexaGlobalCatalog.VALUE_MINIMUM) + if index == max_value: + labels.append(AlexaGlobalCatalog.VALUE_MAXIMUM) + + if len(labels) > 0: + self._resource.add_preset(value=index, labels=labels) + + return self._resource.serialize_capability_resources() + + # Cover Position Resources + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + self._resource = AlexaPresetResource( + ["Position", AlexaGlobalCatalog.SETTING_OPENING], + min_value=0, + max_value=100, + precision=1, + unit=AlexaGlobalCatalog.UNIT_PERCENT, + ) + return self._resource.serialize_capability_resources() + + # Cover Tilt Resources + if self.instance == f"{cover.DOMAIN}.tilt": + self._resource = AlexaPresetResource( + ["Tilt", "Angle", AlexaGlobalCatalog.SETTING_DIRECTION], + min_value=0, + max_value=100, + precision=1, + unit=AlexaGlobalCatalog.UNIT_PERCENT, + ) + return self._resource.serialize_capability_resources() + + # Input Number Value + if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": + min_value = float(self.entity.attributes[input_number.ATTR_MIN]) + max_value = float(self.entity.attributes[input_number.ATTR_MAX]) + precision = float(self.entity.attributes.get(input_number.ATTR_STEP, 1)) + unit = self.entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + + self._resource = AlexaPresetResource( + ["Value", AlexaGlobalCatalog.SETTING_PRESET], + min_value=min_value, + max_value=max_value, + precision=precision, + unit=unit, + ) + self._resource.add_preset( + value=min_value, labels=[AlexaGlobalCatalog.VALUE_MINIMUM] + ) + self._resource.add_preset( + value=max_value, labels=[AlexaGlobalCatalog.VALUE_MAXIMUM] + ) + return self._resource.serialize_capability_resources() + + # Vacuum Fan Speed Resources + if self.instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}": + speed_list = self.entity.attributes[vacuum.ATTR_FAN_SPEED_LIST] + max_value = len(speed_list) - 1 + self._resource = AlexaPresetResource( + labels=[AlexaGlobalCatalog.SETTING_FAN_SPEED], + min_value=0, + max_value=max_value, + precision=1, + ) + for index, speed in enumerate(speed_list): + labels = [speed.replace("_", " ")] + if index == 1: + labels.append(AlexaGlobalCatalog.VALUE_MINIMUM) + if index == max_value: + labels.append(AlexaGlobalCatalog.VALUE_MAXIMUM) + self._resource.add_preset(value=index, labels=labels) + + return self._resource.serialize_capability_resources() + + return None + + def semantics(self): + """Build and return semantics object.""" + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + # Cover Position + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + lower_labels = [AlexaSemantics.ACTION_LOWER] + raise_labels = [AlexaSemantics.ACTION_RAISE] + self._semantics = AlexaSemantics() + + # Add open/close semantics if tilt is not supported. + if not supported & cover.SUPPORT_SET_TILT_POSITION: + lower_labels.append(AlexaSemantics.ACTION_CLOSE) + raise_labels.append(AlexaSemantics.ACTION_OPEN) + self._semantics.add_states_to_value( + [AlexaSemantics.STATES_CLOSED], value=0 + ) + self._semantics.add_states_to_range( + [AlexaSemantics.STATES_OPEN], min_value=1, max_value=100 + ) + + self._semantics.add_action_to_directive( + lower_labels, "SetRangeValue", {"rangeValue": 0} + ) + self._semantics.add_action_to_directive( + raise_labels, "SetRangeValue", {"rangeValue": 100} + ) + return self._semantics.serialize_semantics() + + # Cover Tilt + if self.instance == f"{cover.DOMAIN}.tilt": + self._semantics = AlexaSemantics() + self._semantics.add_action_to_directive( + [AlexaSemantics.ACTION_CLOSE], "SetRangeValue", {"rangeValue": 0} + ) + self._semantics.add_action_to_directive( + [AlexaSemantics.ACTION_OPEN], "SetRangeValue", {"rangeValue": 100} + ) + self._semantics.add_states_to_value([AlexaSemantics.STATES_CLOSED], value=0) + self._semantics.add_states_to_range( + [AlexaSemantics.STATES_OPEN], min_value=1, max_value=100 + ) + return self._semantics.serialize_semantics() + + return None + + +class AlexaToggleController(AlexaCapability): + """Implements Alexa.ToggleController. + + The instance property must be unique across ModeController, RangeController, ToggleController within the same device. + The instance property should be a concatenated string of device domain period and single word. + e.g. fan.speed & fan.direction. + + The instance property must not contain words from other instance property strings within the same device. + e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail. + + An instance property string value may be reused for different devices. + + https://developer.amazon.com/docs/device-apis/alexa-togglecontroller.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "fr-CA", + "fr-FR", + "it-IT", + "ja-JP", + "pt-BR", + } + + def __init__(self, entity, instance, non_controllable=False): + """Initialize the entity.""" + super().__init__(entity, instance) + self._resource = None + self._semantics = None + self.properties_non_controllable = lambda: non_controllable + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.ToggleController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "toggleState"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "toggleState": + raise UnsupportedProperty(name) + + # Fan Oscillating + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": + is_on = bool(self.entity.attributes.get(fan.ATTR_OSCILLATING)) + return "ON" if is_on else "OFF" + + return None + + def capability_resources(self): + """Return capabilityResources object.""" + + # Fan Oscillating Resource + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": + self._resource = AlexaCapabilityResource( + [AlexaGlobalCatalog.SETTING_OSCILLATE, "Rotate", "Rotation"] + ) + return self._resource.serialize_capability_resources() + + return None + + +class AlexaChannelController(AlexaCapability): + """Implements Alexa.ChannelController. + + https://developer.amazon.com/docs/device-apis/alexa-channelcontroller.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "fr-FR", + "hi-IN", + "it-IT", + "ja-JP", + "pt-BR", + } + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.ChannelController" + + +class AlexaDoorbellEventSource(AlexaCapability): + """Implements Alexa.DoorbellEventSource. + + https://developer.amazon.com/docs/device-apis/alexa-doorbelleventsource.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "es-US", + "fr-CA", + "fr-FR", + "hi-IN", + "it-IT", + "ja-JP", + } + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.DoorbellEventSource" + + def capability_proactively_reported(self): + """Return True for proactively reported capability.""" + return True + + +class AlexaPlaybackStateReporter(AlexaCapability): + """Implements Alexa.PlaybackStateReporter. + + https://developer.amazon.com/docs/device-apis/alexa-playbackstatereporter.html + """ + + supported_locales = {"de-DE", "en-GB", "en-US", "es-MX", "fr-FR"} + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.PlaybackStateReporter" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "playbackState"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "playbackState": + raise UnsupportedProperty(name) + + playback_state = self.entity.state + if playback_state == STATE_PLAYING: + return {"state": "PLAYING"} + if playback_state == STATE_PAUSED: + return {"state": "PAUSED"} + + return {"state": "STOPPED"} + + +class AlexaSeekController(AlexaCapability): + """Implements Alexa.SeekController. + + https://developer.amazon.com/docs/device-apis/alexa-seekcontroller.html + """ + + supported_locales = {"de-DE", "en-GB", "en-US", "es-MX"} + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.SeekController" + + +class AlexaEventDetectionSensor(AlexaCapability): + """Implements Alexa.EventDetectionSensor. + + https://developer.amazon.com/docs/device-apis/alexa-eventdetectionsensor.html + """ + + supported_locales = {"en-US"} + + def __init__(self, hass, entity): + """Initialize the entity.""" + super().__init__(entity) + self.hass = hass + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.EventDetectionSensor" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "humanPresenceDetectionState"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "humanPresenceDetectionState": + raise UnsupportedProperty(name) + + human_presence = "NOT_DETECTED" + state = self.entity.state + + # Return None for unavailable and unknown states. + # Allows the Alexa.EndpointHealth Interface to handle the unavailable state in a stateReport. + if state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): + return None + + if self.entity.domain == image_processing.DOMAIN: + if int(state): + human_presence = "DETECTED" + elif state == STATE_ON: + human_presence = "DETECTED" + + return {"value": human_presence} + + def configuration(self): + """Return supported detection types.""" + return { + "detectionMethods": ["AUDIO", "VIDEO"], + "detectionModes": { + "humanPresence": { + "featureAvailability": "ENABLED", + "supportsNotDetected": True, + } + }, + } + + +class AlexaEqualizerController(AlexaCapability): + """Implements Alexa.EqualizerController. + + https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-equalizercontroller.html + """ + + supported_locales = {"de-DE", "en-IN", "en-US", "es-ES", "it-IT", "ja-JP", "pt-BR"} + VALID_SOUND_MODES = { + "MOVIE", + "MUSIC", + "NIGHT", + "SPORT", + "TV", + } + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.EqualizerController" + + def properties_supported(self): + """Return what properties this entity supports. + + Either bands, mode or both can be specified. Only mode is supported at this time. + """ + return [{"name": "mode"}] + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "mode": + raise UnsupportedProperty(name) + + sound_mode = self.entity.attributes.get(media_player.ATTR_SOUND_MODE) + if sound_mode and sound_mode.upper() in self.VALID_SOUND_MODES: + return sound_mode.upper() + + return None + + def configurations(self): + """Return the sound modes supported in the configurations object.""" + configurations = None + supported_sound_modes = self.get_valid_inputs( + self.entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST, []) + ) + if supported_sound_modes: + configurations = {"modes": {"supported": supported_sound_modes}} + + return configurations + + @classmethod + def get_valid_inputs(cls, sound_mode_list): + """Return list of supported inputs.""" + input_list = [] + for sound_mode in sound_mode_list: + sound_mode = sound_mode.upper() + + if sound_mode in cls.VALID_SOUND_MODES: + input_list.append({"name": sound_mode}) + + return input_list + + +class AlexaTimeHoldController(AlexaCapability): + """Implements Alexa.TimeHoldController. + + https://developer.amazon.com/docs/device-apis/alexa-timeholdcontroller.html + """ + + supported_locales = {"en-US"} + + def __init__(self, entity, allow_remote_resume=False): + """Initialize the entity.""" + super().__init__(entity) + self._allow_remote_resume = allow_remote_resume + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.TimeHoldController" + + def configuration(self): + """Return configuration object. + + Set allowRemoteResume to True if Alexa can restart the operation on the device. + When false, Alexa does not send the Resume directive. + """ + return {"allowRemoteResume": self._allow_remote_resume} + + +class AlexaCameraStreamController(AlexaCapability): + """Implements Alexa.CameraStreamController. + + https://developer.amazon.com/docs/device-apis/alexa-camerastreamcontroller.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fr-FR", + "hi-IN", + "it-IT", + "ja-JP", + "pt-BR", + } + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.CameraStreamController" + + def camera_stream_configurations(self): + """Return cameraStreamConfigurations object.""" + return [ + { + "protocols": ["HLS"], + "resolutions": [{"width": 1280, "height": 720}], + "authorizationTypes": ["NONE"], + "videoCodecs": ["H264"], + "audioCodecs": ["AAC"], + } + ] diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/config.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/config.py new file mode 100644 index 00000000000..cc5c604dc8c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/config.py @@ -0,0 +1,89 @@ +"""Config helpers for Alexa.""" +from abc import ABC, abstractmethod + +from homeassistant.core import callback + +from .state_report import async_enable_proactive_mode + + +class AbstractConfig(ABC): + """Hold the configuration for Alexa.""" + + _unsub_proactive_report = None + + def __init__(self, hass): + """Initialize abstract config.""" + self.hass = hass + + @property + def supports_auth(self): + """Return if config supports auth.""" + return False + + @property + def should_report_state(self): + """Return if states should be proactively reported.""" + return False + + @property + def endpoint(self): + """Endpoint for report state.""" + return None + + @property + @abstractmethod + def locale(self): + """Return config locale.""" + + @property + def entity_config(self): + """Return entity config.""" + return {} + + @property + def is_reporting_states(self): + """Return if proactive mode is enabled.""" + return self._unsub_proactive_report is not None + + @callback + @abstractmethod + def user_identifier(self): + """Return an identifier for the user that represents this config.""" + + async def async_enable_proactive_mode(self): + """Enable proactive mode.""" + if self._unsub_proactive_report is None: + self._unsub_proactive_report = self.hass.async_create_task( + async_enable_proactive_mode(self.hass, self) + ) + try: + await self._unsub_proactive_report + except Exception: + self._unsub_proactive_report = None + raise + + async def async_disable_proactive_mode(self): + """Disable proactive mode.""" + unsub_func = await self._unsub_proactive_report + if unsub_func: + unsub_func() + self._unsub_proactive_report = None + + @callback + def should_expose(self, entity_id): + """If an entity should be exposed.""" + # pylint: disable=no-self-use + return False + + @callback + def async_invalidate_access_token(self): + """Invalidate access token.""" + raise NotImplementedError + + async def async_get_access_token(self): + """Get an access token.""" + raise NotImplementedError + + async def async_accept_grant(self, code): + """Accept a grant.""" + raise NotImplementedError diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/const.py new file mode 100644 index 00000000000..de8a4a6fdc4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/const.py @@ -0,0 +1,205 @@ +"""Constants for the Alexa integration.""" +from collections import OrderedDict + +from homeassistant.components.climate import const as climate +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT + +DOMAIN = "alexa" +EVENT_ALEXA_SMART_HOME = "alexa_smart_home" + +# Flash briefing constants +CONF_UID = "uid" +CONF_TITLE = "title" +CONF_AUDIO = "audio" +CONF_TEXT = "text" +CONF_DISPLAY_URL = "display_url" + +CONF_FILTER = "filter" +CONF_ENTITY_CONFIG = "entity_config" +CONF_ENDPOINT = "endpoint" +CONF_LOCALE = "locale" + +ATTR_UID = "uid" +ATTR_UPDATE_DATE = "updateDate" +ATTR_TITLE_TEXT = "titleText" +ATTR_STREAM_URL = "streamUrl" +ATTR_MAIN_TEXT = "mainText" +ATTR_REDIRECTION_URL = "redirectionURL" + +SYN_RESOLUTION_MATCH = "ER_SUCCESS_MATCH" + +DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.0Z" + +API_DIRECTIVE = "directive" +API_ENDPOINT = "endpoint" +API_EVENT = "event" +API_CONTEXT = "context" +API_HEADER = "header" +API_PAYLOAD = "payload" +API_SCOPE = "scope" +API_CHANGE = "change" +API_PASSWORD = "password" + +CONF_DISPLAY_CATEGORIES = "display_categories" +CONF_SUPPORTED_LOCALES = ( + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "es-US", + "fr-CA", + "fr-FR", + "hi-IN", + "it-IT", + "ja-JP", + "pt-BR", +) + +API_TEMP_UNITS = {TEMP_FAHRENHEIT: "FAHRENHEIT", TEMP_CELSIUS: "CELSIUS"} + +# Needs to be ordered dict for `async_api_set_thermostat_mode` which does a +# reverse mapping of this dict and we want to map the first occurrence of OFF +# back to HA state. +API_THERMOSTAT_MODES = OrderedDict( + [ + (climate.HVAC_MODE_HEAT, "HEAT"), + (climate.HVAC_MODE_COOL, "COOL"), + (climate.HVAC_MODE_HEAT_COOL, "AUTO"), + (climate.HVAC_MODE_AUTO, "AUTO"), + (climate.HVAC_MODE_OFF, "OFF"), + (climate.HVAC_MODE_FAN_ONLY, "OFF"), + (climate.HVAC_MODE_DRY, "CUSTOM"), + ] +) +API_THERMOSTAT_MODES_CUSTOM = {climate.HVAC_MODE_DRY: "DEHUMIDIFY"} +API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"} + + +class Cause: + """Possible causes for property changes. + + https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#cause-object + """ + + # Indicates that the event was caused by a customer interaction with an + # application. For example, a customer switches on a light, or locks a door + # using the Alexa app or an app provided by a device vendor. + APP_INTERACTION = "APP_INTERACTION" + + # Indicates that the event was caused by a physical interaction with an + # endpoint. For example manually switching on a light or manually locking a + # door lock + PHYSICAL_INTERACTION = "PHYSICAL_INTERACTION" + + # Indicates that the event was caused by the periodic poll of an appliance, + # which found a change in value. For example, you might poll a temperature + # sensor every hour, and send the updated temperature to Alexa. + PERIODIC_POLL = "PERIODIC_POLL" + + # Indicates that the event was caused by the application of a device rule. + # For example, a customer configures a rule to switch on a light if a + # motion sensor detects motion. In this case, Alexa receives an event from + # the motion sensor, and another event from the light to indicate that its + # state change was caused by the rule. + RULE_TRIGGER = "RULE_TRIGGER" + + # Indicates that the event was caused by a voice interaction with Alexa. + # For example a user speaking to their Echo device. + VOICE_INTERACTION = "VOICE_INTERACTION" + + +class Inputs: + """Valid names for the InputController. + + https://developer.amazon.com/docs/device-apis/alexa-property-schemas.html#input + """ + + VALID_SOURCE_NAME_MAP = { + "antenna": "TUNER", + "antennatv": "TUNER", + "aux": "AUX 1", + "aux1": "AUX 1", + "aux2": "AUX 2", + "aux3": "AUX 3", + "aux4": "AUX 4", + "aux5": "AUX 5", + "aux6": "AUX 6", + "aux7": "AUX 7", + "bluray": "BLURAY", + "blurayplayer": "BLURAY", + "cable": "CABLE", + "cd": "CD", + "coax": "COAX 1", + "coax1": "COAX 1", + "coax2": "COAX 2", + "composite": "COMPOSITE 1", + "composite1": "COMPOSITE 1", + "dvd": "DVD", + "game": "GAME", + "gameconsole": "GAME", + "hdradio": "HD RADIO", + "hdmi": "HDMI 1", + "hdmi1": "HDMI 1", + "hdmi2": "HDMI 2", + "hdmi3": "HDMI 3", + "hdmi4": "HDMI 4", + "hdmi5": "HDMI 5", + "hdmi6": "HDMI 6", + "hdmi7": "HDMI 7", + "hdmi8": "HDMI 8", + "hdmi9": "HDMI 9", + "hdmi10": "HDMI 10", + "hdmiarc": "HDMI ARC", + "input": "INPUT 1", + "input1": "INPUT 1", + "input2": "INPUT 2", + "input3": "INPUT 3", + "input4": "INPUT 4", + "input5": "INPUT 5", + "input6": "INPUT 6", + "input7": "INPUT 7", + "input8": "INPUT 8", + "input9": "INPUT 9", + "input10": "INPUT 10", + "ipod": "IPOD", + "line": "LINE 1", + "line1": "LINE 1", + "line2": "LINE 2", + "line3": "LINE 3", + "line4": "LINE 4", + "line5": "LINE 5", + "line6": "LINE 6", + "line7": "LINE 7", + "mediaplayer": "MEDIA PLAYER", + "optical": "OPTICAL 1", + "optical1": "OPTICAL 1", + "optical2": "OPTICAL 2", + "phono": "PHONO", + "playstation": "PLAYSTATION", + "playstation3": "PLAYSTATION 3", + "playstation4": "PLAYSTATION 4", + "rokumediaplayer": "MEDIA PLAYER", + "satellite": "SATELLITE", + "satellitetv": "SATELLITE", + "smartcast": "SMARTCAST", + "tuner": "TUNER", + "tv": "TV", + "usbdac": "USB DAC", + "video": "VIDEO 1", + "video1": "VIDEO 1", + "video2": "VIDEO 2", + "video3": "VIDEO 3", + "xbox": "XBOX", + } + + VALID_SOUND_MODE_MAP = { + "movie": "MOVIE", + "music": "MUSIC", + "night": "NIGHT", + "sport": "SPORT", + "tv": "TV", + } diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/entities.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/entities.py new file mode 100644 index 00000000000..f31901ce037 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/entities.py @@ -0,0 +1,885 @@ +"""Alexa entity adapters.""" +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +from homeassistant.components import ( + alarm_control_panel, + alert, + automation, + binary_sensor, + camera, + cover, + fan, + group, + image_processing, + input_boolean, + input_number, + light, + lock, + media_player, + scene, + script, + sensor, + switch, + timer, + vacuum, +) +from homeassistant.components.climate import const as climate +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_SUPPORTED_FEATURES, + ATTR_UNIT_OF_MEASUREMENT, + CLOUD_NEVER_EXPOSED_ENTITIES, + CONF_DESCRIPTION, + CONF_NAME, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + __version__, +) +from homeassistant.core import HomeAssistant, State, callback +from homeassistant.helpers import network +from homeassistant.util.decorator import Registry + +from .capabilities import ( + Alexa, + AlexaBrightnessController, + AlexaCameraStreamController, + AlexaCapability, + AlexaChannelController, + AlexaColorController, + AlexaColorTemperatureController, + AlexaContactSensor, + AlexaDoorbellEventSource, + AlexaEndpointHealth, + AlexaEqualizerController, + AlexaEventDetectionSensor, + AlexaInputController, + AlexaLockController, + AlexaModeController, + AlexaMotionSensor, + AlexaPercentageController, + AlexaPlaybackController, + AlexaPlaybackStateReporter, + AlexaPowerController, + AlexaPowerLevelController, + AlexaRangeController, + AlexaSceneController, + AlexaSecurityPanelController, + AlexaSeekController, + AlexaSpeaker, + AlexaStepSpeaker, + AlexaTemperatureSensor, + AlexaThermostatController, + AlexaTimeHoldController, + AlexaToggleController, +) +from .const import CONF_DISPLAY_CATEGORIES + +if TYPE_CHECKING: + from .config import AbstractConfig + +_LOGGER = logging.getLogger(__name__) + +ENTITY_ADAPTERS = Registry() + +TRANSLATION_TABLE = dict.fromkeys(map(ord, r"}{\/|\"()[]+~!><*%"), None) + + +class DisplayCategory: + """Possible display categories for Discovery response. + + https://developer.amazon.com/docs/device-apis/alexa-discovery.html#display-categories + """ + + # Describes a combination of devices set to a specific state, when the + # state change must occur in a specific order. For example, a "watch + # Netflix" scene might require the: 1. TV to be powered on & 2. Input set + # to HDMI1. Applies to Scenes + ACTIVITY_TRIGGER = "ACTIVITY_TRIGGER" + + # Indicates a device that emits pleasant odors and masks unpleasant odors in interior spaces. + AIR_FRESHENER = "AIR_FRESHENER" + + # Indicates a device that improves the quality of air in interior spaces. + AIR_PURIFIER = "AIR_PURIFIER" + + # Indicates a smart device in an automobile, such as a dash camera. + AUTO_ACCESSORY = "AUTO_ACCESSORY" + + # Indicates a security device with video or photo functionality. + CAMERA = "CAMERA" + + # Indicates a religious holiday decoration that often contains lights. + CHRISTMAS_TREE = "CHRISTMAS_TREE" + + # Indicates a device that makes coffee. + COFFEE_MAKER = "COFFEE_MAKER" + + # Indicates a non-mobile computer, such as a desktop computer. + COMPUTER = "COMPUTER" + + # Indicates an endpoint that detects and reports contact. + CONTACT_SENSOR = "CONTACT_SENSOR" + + # Indicates a door. + DOOR = "DOOR" + + # Indicates a doorbell. + DOORBELL = "DOORBELL" + + # Indicates a window covering on the outside of a structure. + EXTERIOR_BLIND = "EXTERIOR_BLIND" + + # Indicates a fan. + FAN = "FAN" + + # Indicates a game console, such as Microsoft Xbox or Nintendo Switch + GAME_CONSOLE = "GAME_CONSOLE" + + # Indicates a garage door. + # Garage doors must implement the ModeController interface to open and close the door. + GARAGE_DOOR = "GARAGE_DOOR" + + # Indicates a wearable device that transmits audio directly into the ear. + HEADPHONES = "HEADPHONES" + + # Indicates a smart-home hub. + HUB = "HUB" + + # Indicates a window covering on the inside of a structure. + INTERIOR_BLIND = "INTERIOR_BLIND" + + # Indicates a laptop or other mobile computer. + LAPTOP = "LAPTOP" + + # Indicates light sources or fixtures. + LIGHT = "LIGHT" + + # Indicates a microwave oven. + MICROWAVE = "MICROWAVE" + + # Indicates a mobile phone. + MOBILE_PHONE = "MOBILE_PHONE" + + # Indicates an endpoint that detects and reports motion. + MOTION_SENSOR = "MOTION_SENSOR" + + # Indicates a network-connected music system. + MUSIC_SYSTEM = "MUSIC_SYSTEM" + + # Indicates a network router. + NETWORK_HARDWARE = "NETWORK_HARDWARE" + + # An endpoint that cannot be described in on of the other categories. + OTHER = "OTHER" + + # Indicates an oven cooking appliance. + OVEN = "OVEN" + + # Indicates a non-mobile phone, such as landline or an IP phone. + PHONE = "PHONE" + + # Indicates a device that prints. + PRINTER = "PRINTER" + + # Indicates a network router. + ROUTER = "ROUTER" + + # Describes a combination of devices set to a specific state, when the + # order of the state change is not important. For example a bedtime scene + # might include turning off lights and lowering the thermostat, but the + # order is unimportant. Applies to Scenes + SCENE_TRIGGER = "SCENE_TRIGGER" + + # Indicates a projector screen. + SCREEN = "SCREEN" + + # Indicates a security panel. + SECURITY_PANEL = "SECURITY_PANEL" + + # Indicates a security system. + SECURITY_SYSTEM = "SECURITY_SYSTEM" + + # Indicates an electric cooking device that sits on a countertop, cooks at low temperatures, + # and is often shaped like a cooking pot. + SLOW_COOKER = "SLOW_COOKER" + + # Indicates an endpoint that locks. + SMARTLOCK = "SMARTLOCK" + + # Indicates modules that are plugged into an existing electrical outlet. + # Can control a variety of devices. + SMARTPLUG = "SMARTPLUG" + + # Indicates the endpoint is a speaker or speaker system. + SPEAKER = "SPEAKER" + + # Indicates a streaming device such as Apple TV, Chromecast, or Roku. + STREAMING_DEVICE = "STREAMING_DEVICE" + + # Indicates in-wall switches wired to the electrical system. Can control a + # variety of devices. + SWITCH = "SWITCH" + + # Indicates a tablet computer. + TABLET = "TABLET" + + # Indicates endpoints that report the temperature only. + TEMPERATURE_SENSOR = "TEMPERATURE_SENSOR" + + # Indicates endpoints that control temperature, stand-alone air + # conditioners, or heaters with direct temperature control. + THERMOSTAT = "THERMOSTAT" + + # Indicates the endpoint is a television. + TV = "TV" + + # Indicates a vacuum cleaner. + VACUUM_CLEANER = "VACUUM_CLEANER" + + # Indicates a network-connected wearable device, such as an Apple Watch, Fitbit, or Samsung Gear. + WEARABLE = "WEARABLE" + + +def generate_alexa_id(entity_id: str) -> str: + """Return the alexa ID for an entity ID.""" + return entity_id.replace(".", "#").translate(TRANSLATION_TABLE) + + +class AlexaEntity: + """An adaptation of an entity, expressed in Alexa's terms. + + The API handlers should manipulate entities only through this interface. + """ + + def __init__(self, hass: HomeAssistant, config: AbstractConfig, entity: State): + """Initialize Alexa Entity.""" + self.hass = hass + self.config = config + self.entity = entity + self.entity_conf = config.entity_config.get(entity.entity_id, {}) + + @property + def entity_id(self): + """Return the Entity ID.""" + return self.entity.entity_id + + def friendly_name(self): + """Return the Alexa API friendly name.""" + return self.entity_conf.get(CONF_NAME, self.entity.name).translate( + TRANSLATION_TABLE + ) + + def description(self): + """Return the Alexa API description.""" + description = self.entity_conf.get(CONF_DESCRIPTION) or self.entity_id + return f"{description} via Home Assistant".translate(TRANSLATION_TABLE) + + def alexa_id(self): + """Return the Alexa API entity id.""" + return generate_alexa_id(self.entity.entity_id) + + def display_categories(self): + """Return a list of display categories.""" + entity_conf = self.config.entity_config.get(self.entity.entity_id, {}) + if CONF_DISPLAY_CATEGORIES in entity_conf: + return [entity_conf[CONF_DISPLAY_CATEGORIES]] + return self.default_display_categories() + + def default_display_categories(self): + """Return a list of default display categories. + + This can be overridden by the user in the Home Assistant configuration. + + See also DisplayCategory. + """ + raise NotImplementedError + + def get_interface(self, capability) -> AlexaCapability: + """Return the given AlexaInterface. + + Raises _UnsupportedInterface. + """ + + def interfaces(self) -> list[AlexaCapability]: + """Return a list of supported interfaces. + + Used for discovery. The list should contain AlexaInterface instances. + If the list is empty, this entity will not be discovered. + """ + raise NotImplementedError + + def serialize_properties(self): + """Yield each supported property in API format.""" + for interface in self.interfaces(): + if not interface.properties_proactively_reported(): + continue + + yield from interface.serialize_properties() + + def serialize_discovery(self): + """Serialize the entity for discovery.""" + result = { + "displayCategories": self.display_categories(), + "cookie": {}, + "endpointId": self.alexa_id(), + "friendlyName": self.friendly_name(), + "description": self.description(), + "manufacturerName": "Home Assistant", + "additionalAttributes": { + "manufacturer": "Home Assistant", + "model": self.entity.domain, + "softwareVersion": __version__, + "customIdentifier": f"{self.config.user_identifier()}-{self.entity_id}", + }, + } + + locale = self.config.locale + capabilities = [] + + for i in self.interfaces(): + if locale not in i.supported_locales: + continue + + try: + capabilities.append(i.serialize_discovery()) + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Error serializing %s discovery for %s", i.name(), self.entity + ) + + result["capabilities"] = capabilities + + return result + + +@callback +def async_get_entities(hass, config) -> list[AlexaEntity]: + """Return all entities that are supported by Alexa.""" + entities = [] + for state in hass.states.async_all(): + if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: + continue + + if state.domain not in ENTITY_ADAPTERS: + continue + + alexa_entity = ENTITY_ADAPTERS[state.domain](hass, config, state) + + if not list(alexa_entity.interfaces()): + continue + + entities.append(alexa_entity) + + return entities + + +@ENTITY_ADAPTERS.register(alert.DOMAIN) +@ENTITY_ADAPTERS.register(automation.DOMAIN) +@ENTITY_ADAPTERS.register(group.DOMAIN) +@ENTITY_ADAPTERS.register(input_boolean.DOMAIN) +class GenericCapabilities(AlexaEntity): + """A generic, on/off device. + + The choice of last resort. + """ + + def default_display_categories(self): + """Return the display categories for this entity.""" + if self.entity.domain == automation.DOMAIN: + return [DisplayCategory.ACTIVITY_TRIGGER] + + return [DisplayCategory.OTHER] + + def interfaces(self): + """Yield the supported interfaces.""" + return [ + AlexaPowerController(self.entity), + AlexaEndpointHealth(self.hass, self.entity), + Alexa(self.hass), + ] + + +@ENTITY_ADAPTERS.register(switch.DOMAIN) +class SwitchCapabilities(AlexaEntity): + """Class to represent Switch capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) + if device_class == switch.DEVICE_CLASS_OUTLET: + return [DisplayCategory.SMARTPLUG] + + return [DisplayCategory.SWITCH] + + def interfaces(self): + """Yield the supported interfaces.""" + return [ + AlexaPowerController(self.entity), + AlexaEndpointHealth(self.hass, self.entity), + Alexa(self.hass), + ] + + +@ENTITY_ADAPTERS.register(climate.DOMAIN) +class ClimateCapabilities(AlexaEntity): + """Class to represent Climate capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.THERMOSTAT] + + def interfaces(self): + """Yield the supported interfaces.""" + # If we support two modes, one being off, we allow turning on too. + if climate.HVAC_MODE_OFF in self.entity.attributes.get( + climate.ATTR_HVAC_MODES, [] + ): + yield AlexaPowerController(self.entity) + + yield AlexaThermostatController(self.hass, self.entity) + yield AlexaTemperatureSensor(self.hass, self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) + + +@ENTITY_ADAPTERS.register(cover.DOMAIN) +class CoverCapabilities(AlexaEntity): + """Class to represent Cover capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) + if device_class in (cover.DEVICE_CLASS_GARAGE, cover.DEVICE_CLASS_GATE): + return [DisplayCategory.GARAGE_DOOR] + if device_class == cover.DEVICE_CLASS_DOOR: + return [DisplayCategory.DOOR] + if device_class in ( + cover.DEVICE_CLASS_BLIND, + cover.DEVICE_CLASS_SHADE, + cover.DEVICE_CLASS_CURTAIN, + ): + return [DisplayCategory.INTERIOR_BLIND] + if device_class in ( + cover.DEVICE_CLASS_WINDOW, + cover.DEVICE_CLASS_AWNING, + cover.DEVICE_CLASS_SHUTTER, + ): + return [DisplayCategory.EXTERIOR_BLIND] + + return [DisplayCategory.OTHER] + + def interfaces(self): + """Yield the supported interfaces.""" + device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) + if device_class not in (cover.DEVICE_CLASS_GARAGE, cover.DEVICE_CLASS_GATE): + yield AlexaPowerController(self.entity) + + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if supported & cover.SUPPORT_SET_POSITION: + yield AlexaRangeController( + self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}" + ) + elif supported & (cover.SUPPORT_CLOSE | cover.SUPPORT_OPEN): + yield AlexaModeController( + self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}" + ) + if supported & cover.SUPPORT_SET_TILT_POSITION: + yield AlexaRangeController(self.entity, instance=f"{cover.DOMAIN}.tilt") + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) + + +@ENTITY_ADAPTERS.register(light.DOMAIN) +class LightCapabilities(AlexaEntity): + """Class to represent Light capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.LIGHT] + + def interfaces(self): + """Yield the supported interfaces.""" + yield AlexaPowerController(self.entity) + + color_modes = self.entity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) + if light.brightness_supported(color_modes): + yield AlexaBrightnessController(self.entity) + if light.color_supported(color_modes): + yield AlexaColorController(self.entity) + if light.color_temp_supported(color_modes): + yield AlexaColorTemperatureController(self.entity) + + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) + + +@ENTITY_ADAPTERS.register(fan.DOMAIN) +class FanCapabilities(AlexaEntity): + """Class to represent Fan capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.FAN] + + def interfaces(self): + """Yield the supported interfaces.""" + yield AlexaPowerController(self.entity) + + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if supported & fan.SUPPORT_SET_SPEED: + yield AlexaPercentageController(self.entity) + yield AlexaPowerLevelController(self.entity) + yield AlexaRangeController( + self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_SPEED}" + ) + if supported & fan.SUPPORT_OSCILLATE: + yield AlexaToggleController( + self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}" + ) + if supported & fan.SUPPORT_DIRECTION: + yield AlexaModeController( + self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}" + ) + + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) + + +@ENTITY_ADAPTERS.register(lock.DOMAIN) +class LockCapabilities(AlexaEntity): + """Class to represent Lock capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.SMARTLOCK] + + def interfaces(self): + """Yield the supported interfaces.""" + return [ + AlexaLockController(self.entity), + AlexaEndpointHealth(self.hass, self.entity), + Alexa(self.hass), + ] + + +@ENTITY_ADAPTERS.register(media_player.const.DOMAIN) +class MediaPlayerCapabilities(AlexaEntity): + """Class to represent MediaPlayer capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) + if device_class == media_player.DEVICE_CLASS_SPEAKER: + return [DisplayCategory.SPEAKER] + + return [DisplayCategory.TV] + + def interfaces(self): + """Yield the supported interfaces.""" + yield AlexaPowerController(self.entity) + + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if supported & media_player.const.SUPPORT_VOLUME_SET: + yield AlexaSpeaker(self.entity) + elif supported & media_player.const.SUPPORT_VOLUME_STEP: + yield AlexaStepSpeaker(self.entity) + + playback_features = ( + media_player.const.SUPPORT_PLAY + | media_player.const.SUPPORT_PAUSE + | media_player.const.SUPPORT_STOP + | media_player.const.SUPPORT_NEXT_TRACK + | media_player.const.SUPPORT_PREVIOUS_TRACK + ) + if supported & playback_features: + yield AlexaPlaybackController(self.entity) + yield AlexaPlaybackStateReporter(self.entity) + + if supported & media_player.const.SUPPORT_SEEK: + yield AlexaSeekController(self.entity) + + if supported & media_player.SUPPORT_SELECT_SOURCE: + inputs = AlexaInputController.get_valid_inputs( + self.entity.attributes.get( + media_player.const.ATTR_INPUT_SOURCE_LIST, [] + ) + ) + if len(inputs) > 0: + yield AlexaInputController(self.entity) + + if supported & media_player.const.SUPPORT_PLAY_MEDIA: + yield AlexaChannelController(self.entity) + + if supported & media_player.const.SUPPORT_SELECT_SOUND_MODE: + inputs = AlexaEqualizerController.get_valid_inputs( + self.entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST, []) + ) + if len(inputs) > 0: + yield AlexaEqualizerController(self.entity) + + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) + + +@ENTITY_ADAPTERS.register(scene.DOMAIN) +class SceneCapabilities(AlexaEntity): + """Class to represent Scene capabilities.""" + + def description(self): + """Return the Alexa API description.""" + description = AlexaEntity.description(self) + if "scene" not in description.casefold(): + return f"{description} (Scene)" + return description + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.SCENE_TRIGGER] + + def interfaces(self): + """Yield the supported interfaces.""" + return [ + AlexaSceneController(self.entity, supports_deactivation=False), + Alexa(self.hass), + ] + + +@ENTITY_ADAPTERS.register(script.DOMAIN) +class ScriptCapabilities(AlexaEntity): + """Class to represent Script capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.ACTIVITY_TRIGGER] + + def interfaces(self): + """Yield the supported interfaces.""" + return [ + AlexaSceneController(self.entity, supports_deactivation=True), + Alexa(self.hass), + ] + + +@ENTITY_ADAPTERS.register(sensor.DOMAIN) +class SensorCapabilities(AlexaEntity): + """Class to represent Sensor capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + # although there are other kinds of sensors, all but temperature + # sensors are currently ignored. + return [DisplayCategory.TEMPERATURE_SENSOR] + + def interfaces(self): + """Yield the supported interfaces.""" + attrs = self.entity.attributes + if attrs.get(ATTR_UNIT_OF_MEASUREMENT) in (TEMP_FAHRENHEIT, TEMP_CELSIUS): + yield AlexaTemperatureSensor(self.hass, self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) + + +@ENTITY_ADAPTERS.register(binary_sensor.DOMAIN) +class BinarySensorCapabilities(AlexaEntity): + """Class to represent BinarySensor capabilities.""" + + TYPE_CONTACT = "contact" + TYPE_MOTION = "motion" + TYPE_PRESENCE = "presence" + + def default_display_categories(self): + """Return the display categories for this entity.""" + sensor_type = self.get_type() + if sensor_type is self.TYPE_CONTACT: + return [DisplayCategory.CONTACT_SENSOR] + if sensor_type is self.TYPE_MOTION: + return [DisplayCategory.MOTION_SENSOR] + if sensor_type is self.TYPE_PRESENCE: + return [DisplayCategory.CAMERA] + + def interfaces(self): + """Yield the supported interfaces.""" + sensor_type = self.get_type() + if sensor_type is self.TYPE_CONTACT: + yield AlexaContactSensor(self.hass, self.entity) + elif sensor_type is self.TYPE_MOTION: + yield AlexaMotionSensor(self.hass, self.entity) + elif sensor_type is self.TYPE_PRESENCE: + yield AlexaEventDetectionSensor(self.hass, self.entity) + + # yield additional interfaces based on specified display category in config. + entity_conf = self.config.entity_config.get(self.entity.entity_id, {}) + if CONF_DISPLAY_CATEGORIES in entity_conf: + if entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.DOORBELL: + yield AlexaDoorbellEventSource(self.entity) + elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.CONTACT_SENSOR: + yield AlexaContactSensor(self.hass, self.entity) + elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.MOTION_SENSOR: + yield AlexaMotionSensor(self.hass, self.entity) + elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.CAMERA: + yield AlexaEventDetectionSensor(self.hass, self.entity) + + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) + + def get_type(self): + """Return the type of binary sensor.""" + attrs = self.entity.attributes + if attrs.get(ATTR_DEVICE_CLASS) in ( + binary_sensor.DEVICE_CLASS_DOOR, + binary_sensor.DEVICE_CLASS_GARAGE_DOOR, + binary_sensor.DEVICE_CLASS_OPENING, + binary_sensor.DEVICE_CLASS_WINDOW, + ): + return self.TYPE_CONTACT + + if attrs.get(ATTR_DEVICE_CLASS) == binary_sensor.DEVICE_CLASS_MOTION: + return self.TYPE_MOTION + + if attrs.get(ATTR_DEVICE_CLASS) == binary_sensor.DEVICE_CLASS_PRESENCE: + return self.TYPE_PRESENCE + + +@ENTITY_ADAPTERS.register(alarm_control_panel.DOMAIN) +class AlarmControlPanelCapabilities(AlexaEntity): + """Class to represent Alarm capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.SECURITY_PANEL] + + def interfaces(self): + """Yield the supported interfaces.""" + if not self.entity.attributes.get("code_arm_required"): + yield AlexaSecurityPanelController(self.hass, self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) + + +@ENTITY_ADAPTERS.register(image_processing.DOMAIN) +class ImageProcessingCapabilities(AlexaEntity): + """Class to represent image_processing capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.CAMERA] + + def interfaces(self): + """Yield the supported interfaces.""" + yield AlexaEventDetectionSensor(self.hass, self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) + + +@ENTITY_ADAPTERS.register(input_number.DOMAIN) +class InputNumberCapabilities(AlexaEntity): + """Class to represent input_number capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.OTHER] + + def interfaces(self): + """Yield the supported interfaces.""" + + yield AlexaRangeController( + self.entity, instance=f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}" + ) + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) + + +@ENTITY_ADAPTERS.register(timer.DOMAIN) +class TimerCapabilities(AlexaEntity): + """Class to represent Timer capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.OTHER] + + def interfaces(self): + """Yield the supported interfaces.""" + yield AlexaTimeHoldController(self.entity, allow_remote_resume=True) + yield AlexaPowerController(self.entity) + yield Alexa(self.entity) + + +@ENTITY_ADAPTERS.register(vacuum.DOMAIN) +class VacuumCapabilities(AlexaEntity): + """Class to represent vacuum capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.VACUUM_CLEANER] + + def interfaces(self): + """Yield the supported interfaces.""" + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if ( + (supported & vacuum.SUPPORT_TURN_ON) or (supported & vacuum.SUPPORT_START) + ) and ( + (supported & vacuum.SUPPORT_TURN_OFF) + or (supported & vacuum.SUPPORT_RETURN_HOME) + ): + yield AlexaPowerController(self.entity) + + if supported & vacuum.SUPPORT_FAN_SPEED: + yield AlexaRangeController( + self.entity, instance=f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}" + ) + + if supported & vacuum.SUPPORT_PAUSE: + support_resume = bool(supported & vacuum.SUPPORT_START) + yield AlexaTimeHoldController( + self.entity, allow_remote_resume=support_resume + ) + + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) + + +@ENTITY_ADAPTERS.register(camera.DOMAIN) +class CameraCapabilities(AlexaEntity): + """Class to represent Camera capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.CAMERA] + + def interfaces(self): + """Yield the supported interfaces.""" + if self._check_requirements(): + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if supported & camera.SUPPORT_STREAM: + yield AlexaCameraStreamController(self.entity) + + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) + + def _check_requirements(self): + """Check the hass URL for HTTPS scheme.""" + if "stream" not in self.hass.config.components: + _LOGGER.debug( + "%s requires stream component for AlexaCameraStreamController", + self.entity_id, + ) + return False + + try: + network.get_url( + self.hass, + allow_internal=False, + allow_ip=False, + require_ssl=True, + require_standard_port=True, + ) + except network.NoURLAvailableError: + _LOGGER.debug( + "%s requires HTTPS for AlexaCameraStreamController", self.entity_id + ) + return False + + return True diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/errors.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/errors.py new file mode 100644 index 00000000000..29643bacc53 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/errors.py @@ -0,0 +1,120 @@ +"""Alexa related errors.""" +from homeassistant.exceptions import HomeAssistantError + +from .const import API_TEMP_UNITS + + +class UnsupportedInterface(HomeAssistantError): + """This entity does not support the requested Smart Home API interface.""" + + +class UnsupportedProperty(HomeAssistantError): + """This entity does not support the requested Smart Home API property.""" + + +class NoTokenAvailable(HomeAssistantError): + """There is no access token available.""" + + +class AlexaError(Exception): + """Base class for errors that can be serialized for the Alexa API. + + A handler can raise subclasses of this to return an error to the request. + """ + + namespace = None + error_type = None + + def __init__(self, error_message, payload=None): + """Initialize an alexa error.""" + Exception.__init__(self) + self.error_message = error_message + self.payload = None + + +class AlexaInvalidEndpointError(AlexaError): + """The endpoint in the request does not exist.""" + + namespace = "Alexa" + error_type = "NO_SUCH_ENDPOINT" + + def __init__(self, endpoint_id): + """Initialize invalid endpoint error.""" + msg = f"The endpoint {endpoint_id} does not exist" + AlexaError.__init__(self, msg) + self.endpoint_id = endpoint_id + + +class AlexaInvalidValueError(AlexaError): + """Class to represent InvalidValue errors.""" + + namespace = "Alexa" + error_type = "INVALID_VALUE" + + +class AlexaUnsupportedThermostatModeError(AlexaError): + """Class to represent UnsupportedThermostatMode errors.""" + + namespace = "Alexa.ThermostatController" + error_type = "UNSUPPORTED_THERMOSTAT_MODE" + + +class AlexaTempRangeError(AlexaError): + """Class to represent TempRange errors.""" + + namespace = "Alexa" + error_type = "TEMPERATURE_VALUE_OUT_OF_RANGE" + + def __init__(self, hass, temp, min_temp, max_temp): + """Initialize TempRange error.""" + unit = hass.config.units.temperature_unit + temp_range = { + "minimumValue": {"value": min_temp, "scale": API_TEMP_UNITS[unit]}, + "maximumValue": {"value": max_temp, "scale": API_TEMP_UNITS[unit]}, + } + payload = {"validRange": temp_range} + msg = f"The requested temperature {temp} is out of range" + + AlexaError.__init__(self, msg, payload) + + +class AlexaBridgeUnreachableError(AlexaError): + """Class to represent BridgeUnreachable errors.""" + + namespace = "Alexa" + error_type = "BRIDGE_UNREACHABLE" + + +class AlexaSecurityPanelUnauthorizedError(AlexaError): + """Class to represent SecurityPanelController Unauthorized errors.""" + + namespace = "Alexa.SecurityPanelController" + error_type = "UNAUTHORIZED" + + +class AlexaSecurityPanelAuthorizationRequired(AlexaError): + """Class to represent SecurityPanelController AuthorizationRequired errors.""" + + namespace = "Alexa.SecurityPanelController" + error_type = "AUTHORIZATION_REQUIRED" + + +class AlexaAlreadyInOperationError(AlexaError): + """Class to represent AlreadyInOperation errors.""" + + namespace = "Alexa" + error_type = "ALREADY_IN_OPERATION" + + +class AlexaInvalidDirectiveError(AlexaError): + """Class to represent InvalidDirective errors.""" + + namespace = "Alexa" + error_type = "INVALID_DIRECTIVE" + + +class AlexaVideoActionNotPermittedForContentError(AlexaError): + """Class to represent action not permitted for content errors.""" + + namespace = "Alexa.Video" + error_type = "ACTION_NOT_PERMITTED_FOR_CONTENT" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/flash_briefings.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/flash_briefings.py new file mode 100644 index 00000000000..50463810bbf --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/flash_briefings.py @@ -0,0 +1,121 @@ +"""Support for Alexa skill service end point.""" +import copy +import hmac +import logging +import uuid + +from homeassistant.components import http +from homeassistant.const import CONF_PASSWORD, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED +from homeassistant.core import callback +from homeassistant.helpers import template +import homeassistant.util.dt as dt_util + +from .const import ( + API_PASSWORD, + ATTR_MAIN_TEXT, + ATTR_REDIRECTION_URL, + ATTR_STREAM_URL, + ATTR_TITLE_TEXT, + ATTR_UID, + ATTR_UPDATE_DATE, + CONF_AUDIO, + CONF_DISPLAY_URL, + CONF_TEXT, + CONF_TITLE, + CONF_UID, + DATE_FORMAT, +) + +_LOGGER = logging.getLogger(__name__) + +FLASH_BRIEFINGS_API_ENDPOINT = "/api/alexa/flash_briefings/{briefing_id}" + + +@callback +def async_setup(hass, flash_briefing_config): + """Activate Alexa component.""" + hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefing_config)) + + +class AlexaFlashBriefingView(http.HomeAssistantView): + """Handle Alexa Flash Briefing skill requests.""" + + url = FLASH_BRIEFINGS_API_ENDPOINT + requires_auth = False + name = "api:alexa:flash_briefings" + + def __init__(self, hass, flash_briefings): + """Initialize Alexa view.""" + super().__init__() + self.flash_briefings = copy.deepcopy(flash_briefings) + template.attach(hass, self.flash_briefings) + + @callback + def get(self, request, briefing_id): + """Handle Alexa Flash Briefing request.""" + _LOGGER.debug("Received Alexa flash briefing request for: %s", briefing_id) + + if request.query.get(API_PASSWORD) is None: + err = "No password provided for Alexa flash briefing: %s" + _LOGGER.error(err, briefing_id) + return b"", HTTP_UNAUTHORIZED + + if not hmac.compare_digest( + request.query[API_PASSWORD].encode("utf-8"), + self.flash_briefings[CONF_PASSWORD].encode("utf-8"), + ): + err = "Wrong password for Alexa flash briefing: %s" + _LOGGER.error(err, briefing_id) + return b"", HTTP_UNAUTHORIZED + + if not isinstance(self.flash_briefings.get(briefing_id), list): + err = "No configured Alexa flash briefing was found for: %s" + _LOGGER.error(err, briefing_id) + return b"", HTTP_NOT_FOUND + + briefing = [] + + for item in self.flash_briefings.get(briefing_id, []): + output = {} + if item.get(CONF_TITLE) is not None: + if isinstance(item.get(CONF_TITLE), template.Template): + output[ATTR_TITLE_TEXT] = item[CONF_TITLE].async_render( + parse_result=False + ) + else: + output[ATTR_TITLE_TEXT] = item.get(CONF_TITLE) + + if item.get(CONF_TEXT) is not None: + if isinstance(item.get(CONF_TEXT), template.Template): + output[ATTR_MAIN_TEXT] = item[CONF_TEXT].async_render( + parse_result=False + ) + else: + output[ATTR_MAIN_TEXT] = item.get(CONF_TEXT) + + uid = item.get(CONF_UID) + if uid is None: + uid = str(uuid.uuid4()) + output[ATTR_UID] = uid + + if item.get(CONF_AUDIO) is not None: + if isinstance(item.get(CONF_AUDIO), template.Template): + output[ATTR_STREAM_URL] = item[CONF_AUDIO].async_render( + parse_result=False + ) + else: + output[ATTR_STREAM_URL] = item.get(CONF_AUDIO) + + if item.get(CONF_DISPLAY_URL) is not None: + if isinstance(item.get(CONF_DISPLAY_URL), template.Template): + output[ATTR_REDIRECTION_URL] = item[CONF_DISPLAY_URL].async_render( + parse_result=False + ) + else: + output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL) + + output[ATTR_UPDATE_DATE] = dt_util.utcnow().strftime(DATE_FORMAT) + + briefing.append(output) + + return self.json(briefing) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/handlers.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/handlers.py new file mode 100644 index 00000000000..da0011f817a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/handlers.py @@ -0,0 +1,1524 @@ +"""Alexa message handlers.""" +import logging +import math + +from homeassistant import core as ha +from homeassistant.components import ( + camera, + cover, + fan, + group, + input_number, + light, + media_player, + timer, + vacuum, +) +from homeassistant.components.climate import const as climate +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_ENTITY_PICTURE, + ATTR_SUPPORTED_FEATURES, + ATTR_TEMPERATURE, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + SERVICE_LOCK, + SERVICE_MEDIA_NEXT_TRACK, + SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_MEDIA_STOP, + SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_UNLOCK, + SERVICE_VOLUME_DOWN, + SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_SET, + SERVICE_VOLUME_UP, + STATE_ALARM_DISARMED, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.helpers import network +import homeassistant.util.color as color_util +from homeassistant.util.decorator import Registry +import homeassistant.util.dt as dt_util +from homeassistant.util.temperature import convert as convert_temperature + +from .const import ( + API_TEMP_UNITS, + API_THERMOSTAT_MODES, + API_THERMOSTAT_MODES_CUSTOM, + API_THERMOSTAT_PRESETS, + Cause, + Inputs, +) +from .entities import async_get_entities +from .errors import ( + AlexaInvalidDirectiveError, + AlexaInvalidValueError, + AlexaSecurityPanelAuthorizationRequired, + AlexaSecurityPanelUnauthorizedError, + AlexaTempRangeError, + AlexaUnsupportedThermostatModeError, + AlexaVideoActionNotPermittedForContentError, +) +from .state_report import async_enable_proactive_mode + +_LOGGER = logging.getLogger(__name__) +HANDLERS = Registry() + + +@HANDLERS.register(("Alexa.Discovery", "Discover")) +async def async_api_discovery(hass, config, directive, context): + """Create a API formatted discovery response. + + Async friendly. + """ + discovery_endpoints = [ + alexa_entity.serialize_discovery() + for alexa_entity in async_get_entities(hass, config) + if config.should_expose(alexa_entity.entity_id) + ] + + return directive.response( + name="Discover.Response", + namespace="Alexa.Discovery", + payload={"endpoints": discovery_endpoints}, + ) + + +@HANDLERS.register(("Alexa.Authorization", "AcceptGrant")) +async def async_api_accept_grant(hass, config, directive, context): + """Create a API formatted AcceptGrant response. + + Async friendly. + """ + auth_code = directive.payload["grant"]["code"] + _LOGGER.debug("AcceptGrant code: %s", auth_code) + + if config.supports_auth: + await config.async_accept_grant(auth_code) + + if config.should_report_state: + await async_enable_proactive_mode(hass, config) + + return directive.response( + name="AcceptGrant.Response", namespace="Alexa.Authorization", payload={} + ) + + +@HANDLERS.register(("Alexa.PowerController", "TurnOn")) +async def async_api_turn_on(hass, config, directive, context): + """Process a turn on request.""" + entity = directive.entity + domain = entity.domain + if domain == group.DOMAIN: + domain = ha.DOMAIN + + service = SERVICE_TURN_ON + if domain == cover.DOMAIN: + service = cover.SERVICE_OPEN_COVER + elif domain == vacuum.DOMAIN: + supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if not supported & vacuum.SUPPORT_TURN_ON and supported & vacuum.SUPPORT_START: + service = vacuum.SERVICE_START + elif domain == timer.DOMAIN: + service = timer.SERVICE_START + elif domain == media_player.DOMAIN: + supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF + if not supported & power_features: + service = media_player.SERVICE_MEDIA_PLAY + + await hass.services.async_call( + domain, + service, + {ATTR_ENTITY_ID: entity.entity_id}, + blocking=False, + context=context, + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.PowerController", "TurnOff")) +async def async_api_turn_off(hass, config, directive, context): + """Process a turn off request.""" + entity = directive.entity + domain = entity.domain + if entity.domain == group.DOMAIN: + domain = ha.DOMAIN + + service = SERVICE_TURN_OFF + if entity.domain == cover.DOMAIN: + service = cover.SERVICE_CLOSE_COVER + elif domain == vacuum.DOMAIN: + supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if ( + not supported & vacuum.SUPPORT_TURN_OFF + and supported & vacuum.SUPPORT_RETURN_HOME + ): + service = vacuum.SERVICE_RETURN_TO_BASE + elif domain == timer.DOMAIN: + service = timer.SERVICE_CANCEL + elif domain == media_player.DOMAIN: + supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF + if not supported & power_features: + service = media_player.SERVICE_MEDIA_STOP + + await hass.services.async_call( + domain, + service, + {ATTR_ENTITY_ID: entity.entity_id}, + blocking=False, + context=context, + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.BrightnessController", "SetBrightness")) +async def async_api_set_brightness(hass, config, directive, context): + """Process a set brightness request.""" + entity = directive.entity + brightness = int(directive.payload["brightness"]) + + await hass.services.async_call( + entity.domain, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity.entity_id, light.ATTR_BRIGHTNESS_PCT: brightness}, + blocking=False, + context=context, + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.BrightnessController", "AdjustBrightness")) +async def async_api_adjust_brightness(hass, config, directive, context): + """Process an adjust brightness request.""" + entity = directive.entity + brightness_delta = int(directive.payload["brightnessDelta"]) + + # read current state + try: + current = math.floor( + int(entity.attributes.get(light.ATTR_BRIGHTNESS)) / 255 * 100 + ) + except ZeroDivisionError: + current = 0 + + # set brightness + brightness = max(0, brightness_delta + current) + await hass.services.async_call( + entity.domain, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity.entity_id, light.ATTR_BRIGHTNESS_PCT: brightness}, + blocking=False, + context=context, + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.ColorController", "SetColor")) +async def async_api_set_color(hass, config, directive, context): + """Process a set color request.""" + entity = directive.entity + rgb = color_util.color_hsb_to_RGB( + float(directive.payload["color"]["hue"]), + float(directive.payload["color"]["saturation"]), + float(directive.payload["color"]["brightness"]), + ) + + await hass.services.async_call( + entity.domain, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity.entity_id, light.ATTR_RGB_COLOR: rgb}, + blocking=False, + context=context, + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.ColorTemperatureController", "SetColorTemperature")) +async def async_api_set_color_temperature(hass, config, directive, context): + """Process a set color temperature request.""" + entity = directive.entity + kelvin = int(directive.payload["colorTemperatureInKelvin"]) + + await hass.services.async_call( + entity.domain, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity.entity_id, light.ATTR_KELVIN: kelvin}, + blocking=False, + context=context, + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.ColorTemperatureController", "DecreaseColorTemperature")) +async def async_api_decrease_color_temp(hass, config, directive, context): + """Process a decrease color temperature request.""" + entity = directive.entity + current = int(entity.attributes.get(light.ATTR_COLOR_TEMP)) + max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS)) + + value = min(max_mireds, current + 50) + await hass.services.async_call( + entity.domain, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity.entity_id, light.ATTR_COLOR_TEMP: value}, + blocking=False, + context=context, + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.ColorTemperatureController", "IncreaseColorTemperature")) +async def async_api_increase_color_temp(hass, config, directive, context): + """Process an increase color temperature request.""" + entity = directive.entity + current = int(entity.attributes.get(light.ATTR_COLOR_TEMP)) + min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS)) + + value = max(min_mireds, current - 50) + await hass.services.async_call( + entity.domain, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity.entity_id, light.ATTR_COLOR_TEMP: value}, + blocking=False, + context=context, + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.SceneController", "Activate")) +async def async_api_activate(hass, config, directive, context): + """Process an activate request.""" + entity = directive.entity + domain = entity.domain + + await hass.services.async_call( + domain, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity.entity_id}, + blocking=False, + context=context, + ) + + payload = { + "cause": {"type": Cause.VOICE_INTERACTION}, + "timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z", + } + + return directive.response( + name="ActivationStarted", namespace="Alexa.SceneController", payload=payload + ) + + +@HANDLERS.register(("Alexa.SceneController", "Deactivate")) +async def async_api_deactivate(hass, config, directive, context): + """Process a deactivate request.""" + entity = directive.entity + domain = entity.domain + + await hass.services.async_call( + domain, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity.entity_id}, + blocking=False, + context=context, + ) + + payload = { + "cause": {"type": Cause.VOICE_INTERACTION}, + "timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z", + } + + return directive.response( + name="DeactivationStarted", namespace="Alexa.SceneController", payload=payload + ) + + +@HANDLERS.register(("Alexa.PercentageController", "SetPercentage")) +async def async_api_set_percentage(hass, config, directive, context): + """Process a set percentage request.""" + entity = directive.entity + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + + if entity.domain == fan.DOMAIN: + service = fan.SERVICE_SET_PERCENTAGE + percentage = int(directive.payload["percentage"]) + data[fan.ATTR_PERCENTAGE] = percentage + + await hass.services.async_call( + entity.domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.PercentageController", "AdjustPercentage")) +async def async_api_adjust_percentage(hass, config, directive, context): + """Process an adjust percentage request.""" + entity = directive.entity + percentage_delta = int(directive.payload["percentageDelta"]) + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + + if entity.domain == fan.DOMAIN: + service = fan.SERVICE_SET_PERCENTAGE + current = entity.attributes.get(fan.ATTR_PERCENTAGE) or 0 + + # set percentage + percentage = min(100, max(0, percentage_delta + current)) + data[fan.ATTR_PERCENTAGE] = percentage + + await hass.services.async_call( + entity.domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.LockController", "Lock")) +async def async_api_lock(hass, config, directive, context): + """Process a lock request.""" + entity = directive.entity + await hass.services.async_call( + entity.domain, + SERVICE_LOCK, + {ATTR_ENTITY_ID: entity.entity_id}, + blocking=False, + context=context, + ) + + response = directive.response() + response.add_context_property( + {"name": "lockState", "namespace": "Alexa.LockController", "value": "LOCKED"} + ) + return response + + +@HANDLERS.register(("Alexa.LockController", "Unlock")) +async def async_api_unlock(hass, config, directive, context): + """Process an unlock request.""" + if config.locale not in {"de-DE", "en-US", "ja-JP"}: + msg = f"The unlock directive is not supported for the following locales: {config.locale}" + raise AlexaInvalidDirectiveError(msg) + + entity = directive.entity + await hass.services.async_call( + entity.domain, + SERVICE_UNLOCK, + {ATTR_ENTITY_ID: entity.entity_id}, + blocking=False, + context=context, + ) + + response = directive.response() + response.add_context_property( + {"namespace": "Alexa.LockController", "name": "lockState", "value": "UNLOCKED"} + ) + + return response + + +@HANDLERS.register(("Alexa.Speaker", "SetVolume")) +async def async_api_set_volume(hass, config, directive, context): + """Process a set volume request.""" + volume = round(float(directive.payload["volume"] / 100), 2) + entity = directive.entity + + data = { + ATTR_ENTITY_ID: entity.entity_id, + media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume, + } + + await hass.services.async_call( + entity.domain, SERVICE_VOLUME_SET, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.InputController", "SelectInput")) +async def async_api_select_input(hass, config, directive, context): + """Process a set input request.""" + media_input = directive.payload["input"] + entity = directive.entity + + # Attempt to map the ALL UPPERCASE payload name to a source. + # Strips trailing 1 to match single input devices. + source_list = entity.attributes.get(media_player.const.ATTR_INPUT_SOURCE_LIST, []) + for source in source_list: + formatted_source = ( + source.lower().replace("-", "").replace("_", "").replace(" ", "") + ) + media_input = media_input.lower().replace(" ", "") + if ( + formatted_source in Inputs.VALID_SOURCE_NAME_MAP + and formatted_source == media_input + ) or ( + media_input.endswith("1") and formatted_source == media_input.rstrip("1") + ): + media_input = source + break + else: + msg = ( + f"failed to map input {media_input} to a media source on {entity.entity_id}" + ) + raise AlexaInvalidValueError(msg) + + data = { + ATTR_ENTITY_ID: entity.entity_id, + media_player.const.ATTR_INPUT_SOURCE: media_input, + } + + await hass.services.async_call( + entity.domain, + media_player.SERVICE_SELECT_SOURCE, + data, + blocking=False, + context=context, + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.Speaker", "AdjustVolume")) +async def async_api_adjust_volume(hass, config, directive, context): + """Process an adjust volume request.""" + volume_delta = int(directive.payload["volume"]) + + entity = directive.entity + current_level = entity.attributes.get(media_player.const.ATTR_MEDIA_VOLUME_LEVEL) + + # read current state + try: + current = math.floor(int(current_level * 100)) + except ZeroDivisionError: + current = 0 + + volume = float(max(0, volume_delta + current) / 100) + + data = { + ATTR_ENTITY_ID: entity.entity_id, + media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume, + } + + await hass.services.async_call( + entity.domain, SERVICE_VOLUME_SET, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.StepSpeaker", "AdjustVolume")) +async def async_api_adjust_volume_step(hass, config, directive, context): + """Process an adjust volume step request.""" + # media_player volume up/down service does not support specifying steps + # each component handles it differently e.g. via config. + # This workaround will simply call the volume up/Volume down the amount of steps asked for + # When no steps are called in the request, Alexa sends a default of 10 steps which for most + # purposes is too high. The default is set 1 in this case. + entity = directive.entity + volume_int = int(directive.payload["volumeSteps"]) + is_default = bool(directive.payload["volumeStepsDefault"]) + default_steps = 1 + + if volume_int < 0: + service_volume = SERVICE_VOLUME_DOWN + if is_default: + volume_int = -default_steps + else: + service_volume = SERVICE_VOLUME_UP + if is_default: + volume_int = default_steps + + data = {ATTR_ENTITY_ID: entity.entity_id} + + for _ in range(abs(volume_int)): + await hass.services.async_call( + entity.domain, service_volume, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.StepSpeaker", "SetMute")) +@HANDLERS.register(("Alexa.Speaker", "SetMute")) +async def async_api_set_mute(hass, config, directive, context): + """Process a set mute request.""" + mute = bool(directive.payload["mute"]) + entity = directive.entity + data = { + ATTR_ENTITY_ID: entity.entity_id, + media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute, + } + + await hass.services.async_call( + entity.domain, SERVICE_VOLUME_MUTE, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.PlaybackController", "Play")) +async def async_api_play(hass, config, directive, context): + """Process a play request.""" + entity = directive.entity + data = {ATTR_ENTITY_ID: entity.entity_id} + + await hass.services.async_call( + entity.domain, SERVICE_MEDIA_PLAY, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.PlaybackController", "Pause")) +async def async_api_pause(hass, config, directive, context): + """Process a pause request.""" + entity = directive.entity + data = {ATTR_ENTITY_ID: entity.entity_id} + + await hass.services.async_call( + entity.domain, SERVICE_MEDIA_PAUSE, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.PlaybackController", "Stop")) +async def async_api_stop(hass, config, directive, context): + """Process a stop request.""" + entity = directive.entity + data = {ATTR_ENTITY_ID: entity.entity_id} + + await hass.services.async_call( + entity.domain, SERVICE_MEDIA_STOP, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.PlaybackController", "Next")) +async def async_api_next(hass, config, directive, context): + """Process a next request.""" + entity = directive.entity + data = {ATTR_ENTITY_ID: entity.entity_id} + + await hass.services.async_call( + entity.domain, SERVICE_MEDIA_NEXT_TRACK, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.PlaybackController", "Previous")) +async def async_api_previous(hass, config, directive, context): + """Process a previous request.""" + entity = directive.entity + data = {ATTR_ENTITY_ID: entity.entity_id} + + await hass.services.async_call( + entity.domain, + SERVICE_MEDIA_PREVIOUS_TRACK, + data, + blocking=False, + context=context, + ) + + return directive.response() + + +def temperature_from_object(hass, temp_obj, interval=False): + """Get temperature from Temperature object in requested unit.""" + to_unit = hass.config.units.temperature_unit + from_unit = TEMP_CELSIUS + temp = float(temp_obj["value"]) + + if temp_obj["scale"] == "FAHRENHEIT": + from_unit = TEMP_FAHRENHEIT + elif temp_obj["scale"] == "KELVIN" and not interval: + # convert to Celsius if absolute temperature + temp -= 273.15 + + return convert_temperature(temp, from_unit, to_unit, interval) + + +@HANDLERS.register(("Alexa.ThermostatController", "SetTargetTemperature")) +async def async_api_set_target_temp(hass, config, directive, context): + """Process a set target temperature request.""" + entity = directive.entity + min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP) + max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP) + unit = hass.config.units.temperature_unit + + data = {ATTR_ENTITY_ID: entity.entity_id} + + payload = directive.payload + response = directive.response() + if "targetSetpoint" in payload: + temp = temperature_from_object(hass, payload["targetSetpoint"]) + if temp < min_temp or temp > max_temp: + raise AlexaTempRangeError(hass, temp, min_temp, max_temp) + data[ATTR_TEMPERATURE] = temp + response.add_context_property( + { + "name": "targetSetpoint", + "namespace": "Alexa.ThermostatController", + "value": {"value": temp, "scale": API_TEMP_UNITS[unit]}, + } + ) + if "lowerSetpoint" in payload: + temp_low = temperature_from_object(hass, payload["lowerSetpoint"]) + if temp_low < min_temp or temp_low > max_temp: + raise AlexaTempRangeError(hass, temp_low, min_temp, max_temp) + data[climate.ATTR_TARGET_TEMP_LOW] = temp_low + response.add_context_property( + { + "name": "lowerSetpoint", + "namespace": "Alexa.ThermostatController", + "value": {"value": temp_low, "scale": API_TEMP_UNITS[unit]}, + } + ) + if "upperSetpoint" in payload: + temp_high = temperature_from_object(hass, payload["upperSetpoint"]) + if temp_high < min_temp or temp_high > max_temp: + raise AlexaTempRangeError(hass, temp_high, min_temp, max_temp) + data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high + response.add_context_property( + { + "name": "upperSetpoint", + "namespace": "Alexa.ThermostatController", + "value": {"value": temp_high, "scale": API_TEMP_UNITS[unit]}, + } + ) + + await hass.services.async_call( + entity.domain, + climate.SERVICE_SET_TEMPERATURE, + data, + blocking=False, + context=context, + ) + + return response + + +@HANDLERS.register(("Alexa.ThermostatController", "AdjustTargetTemperature")) +async def async_api_adjust_target_temp(hass, config, directive, context): + """Process an adjust target temperature request.""" + entity = directive.entity + min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP) + max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP) + unit = hass.config.units.temperature_unit + + temp_delta = temperature_from_object( + hass, directive.payload["targetSetpointDelta"], interval=True + ) + target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta + + if target_temp < min_temp or target_temp > max_temp: + raise AlexaTempRangeError(hass, target_temp, min_temp, max_temp) + + data = {ATTR_ENTITY_ID: entity.entity_id, ATTR_TEMPERATURE: target_temp} + + response = directive.response() + await hass.services.async_call( + entity.domain, + climate.SERVICE_SET_TEMPERATURE, + data, + blocking=False, + context=context, + ) + response.add_context_property( + { + "name": "targetSetpoint", + "namespace": "Alexa.ThermostatController", + "value": {"value": target_temp, "scale": API_TEMP_UNITS[unit]}, + } + ) + + return response + + +@HANDLERS.register(("Alexa.ThermostatController", "SetThermostatMode")) +async def async_api_set_thermostat_mode(hass, config, directive, context): + """Process a set thermostat mode request.""" + entity = directive.entity + mode = directive.payload["thermostatMode"] + mode = mode if isinstance(mode, str) else mode["value"] + + data = {ATTR_ENTITY_ID: entity.entity_id} + + ha_preset = next((k for k, v in API_THERMOSTAT_PRESETS.items() if v == mode), None) + + if ha_preset: + presets = entity.attributes.get(climate.ATTR_PRESET_MODES, []) + + if ha_preset not in presets: + msg = f"The requested thermostat mode {ha_preset} is not supported" + raise AlexaUnsupportedThermostatModeError(msg) + + service = climate.SERVICE_SET_PRESET_MODE + data[climate.ATTR_PRESET_MODE] = ha_preset + + elif mode == "CUSTOM": + operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES) + custom_mode = directive.payload["thermostatMode"]["customName"] + custom_mode = next( + (k for k, v in API_THERMOSTAT_MODES_CUSTOM.items() if v == custom_mode), + None, + ) + if custom_mode not in operation_list: + msg = ( + f"The requested thermostat mode {mode}: {custom_mode} is not supported" + ) + raise AlexaUnsupportedThermostatModeError(msg) + + service = climate.SERVICE_SET_HVAC_MODE + data[climate.ATTR_HVAC_MODE] = custom_mode + + else: + operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES) + ha_modes = {k: v for k, v in API_THERMOSTAT_MODES.items() if v == mode} + ha_mode = next(iter(set(ha_modes).intersection(operation_list)), None) + if ha_mode not in operation_list: + msg = f"The requested thermostat mode {mode} is not supported" + raise AlexaUnsupportedThermostatModeError(msg) + + service = climate.SERVICE_SET_HVAC_MODE + data[climate.ATTR_HVAC_MODE] = ha_mode + + response = directive.response() + await hass.services.async_call( + climate.DOMAIN, service, data, blocking=False, context=context + ) + response.add_context_property( + { + "name": "thermostatMode", + "namespace": "Alexa.ThermostatController", + "value": mode, + } + ) + + return response + + +@HANDLERS.register(("Alexa", "ReportState")) +async def async_api_reportstate(hass, config, directive, context): + """Process a ReportState request.""" + return directive.response(name="StateReport") + + +@HANDLERS.register(("Alexa.PowerLevelController", "SetPowerLevel")) +async def async_api_set_power_level(hass, config, directive, context): + """Process a SetPowerLevel request.""" + entity = directive.entity + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + + if entity.domain == fan.DOMAIN: + service = fan.SERVICE_SET_PERCENTAGE + percentage = int(directive.payload["powerLevel"]) + data[fan.ATTR_PERCENTAGE] = percentage + + await hass.services.async_call( + entity.domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.PowerLevelController", "AdjustPowerLevel")) +async def async_api_adjust_power_level(hass, config, directive, context): + """Process an AdjustPowerLevel request.""" + entity = directive.entity + percentage_delta = int(directive.payload["powerLevelDelta"]) + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + + if entity.domain == fan.DOMAIN: + service = fan.SERVICE_SET_PERCENTAGE + current = entity.attributes.get(fan.ATTR_PERCENTAGE) or 0 + + # set percentage + percentage = min(100, max(0, percentage_delta + current)) + data[fan.ATTR_PERCENTAGE] = percentage + + await hass.services.async_call( + entity.domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.SecurityPanelController", "Arm")) +async def async_api_arm(hass, config, directive, context): + """Process a Security Panel Arm request.""" + entity = directive.entity + service = None + arm_state = directive.payload["armState"] + data = {ATTR_ENTITY_ID: entity.entity_id} + + if entity.state != STATE_ALARM_DISARMED: + msg = "You must disarm the system before you can set the requested arm state." + raise AlexaSecurityPanelAuthorizationRequired(msg) + + if arm_state == "ARMED_AWAY": + service = SERVICE_ALARM_ARM_AWAY + elif arm_state == "ARMED_NIGHT": + service = SERVICE_ALARM_ARM_NIGHT + elif arm_state == "ARMED_STAY": + service = SERVICE_ALARM_ARM_HOME + + await hass.services.async_call( + entity.domain, service, data, blocking=False, context=context + ) + + # return 0 until alarm integration supports an exit delay + payload = {"exitDelayInSeconds": 0} + + response = directive.response( + name="Arm.Response", namespace="Alexa.SecurityPanelController", payload=payload + ) + + response.add_context_property( + { + "name": "armState", + "namespace": "Alexa.SecurityPanelController", + "value": arm_state, + } + ) + + return response + + +@HANDLERS.register(("Alexa.SecurityPanelController", "Disarm")) +async def async_api_disarm(hass, config, directive, context): + """Process a Security Panel Disarm request.""" + entity = directive.entity + data = {ATTR_ENTITY_ID: entity.entity_id} + response = directive.response() + + # Per Alexa Documentation: If you receive a Disarm directive, and the system is already disarmed, + # respond with a success response, not an error response. + if entity.state == STATE_ALARM_DISARMED: + return response + + payload = directive.payload + if "authorization" in payload: + value = payload["authorization"]["value"] + if payload["authorization"]["type"] == "FOUR_DIGIT_PIN": + data["code"] = value + + if not await hass.services.async_call( + entity.domain, SERVICE_ALARM_DISARM, data, blocking=True, context=context + ): + msg = "Invalid Code" + raise AlexaSecurityPanelUnauthorizedError(msg) + + response.add_context_property( + { + "name": "armState", + "namespace": "Alexa.SecurityPanelController", + "value": "DISARMED", + } + ) + + return response + + +@HANDLERS.register(("Alexa.ModeController", "SetMode")) +async def async_api_set_mode(hass, config, directive, context): + """Process a SetMode directive.""" + entity = directive.entity + instance = directive.instance + domain = entity.domain + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + mode = directive.payload["mode"] + + # Fan Direction + if instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": + direction = mode.split(".")[1] + if direction in (fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD): + service = fan.SERVICE_SET_DIRECTION + data[fan.ATTR_DIRECTION] = direction + + # Cover Position + elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + position = mode.split(".")[1] + + if position == cover.STATE_CLOSED: + service = cover.SERVICE_CLOSE_COVER + elif position == cover.STATE_OPEN: + service = cover.SERVICE_OPEN_COVER + elif position == "custom": + service = cover.SERVICE_STOP_COVER + + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + await hass.services.async_call( + domain, service, data, blocking=False, context=context + ) + + response = directive.response() + response.add_context_property( + { + "namespace": "Alexa.ModeController", + "instance": instance, + "name": "mode", + "value": mode, + } + ) + + return response + + +@HANDLERS.register(("Alexa.ModeController", "AdjustMode")) +async def async_api_adjust_mode(hass, config, directive, context): + """Process a AdjustMode request. + + Requires capabilityResources supportedModes to be ordered. + Only supportedModes with ordered=True support the adjustMode directive. + """ + + # Currently no supportedModes are configured with ordered=True to support this request. + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + +@HANDLERS.register(("Alexa.ToggleController", "TurnOn")) +async def async_api_toggle_on(hass, config, directive, context): + """Process a toggle on request.""" + entity = directive.entity + instance = directive.instance + domain = entity.domain + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + + # Fan Oscillating + if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": + service = fan.SERVICE_OSCILLATE + data[fan.ATTR_OSCILLATING] = True + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + await hass.services.async_call( + domain, service, data, blocking=False, context=context + ) + + response = directive.response() + response.add_context_property( + { + "namespace": "Alexa.ToggleController", + "instance": instance, + "name": "toggleState", + "value": "ON", + } + ) + + return response + + +@HANDLERS.register(("Alexa.ToggleController", "TurnOff")) +async def async_api_toggle_off(hass, config, directive, context): + """Process a toggle off request.""" + entity = directive.entity + instance = directive.instance + domain = entity.domain + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + + # Fan Oscillating + if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": + service = fan.SERVICE_OSCILLATE + data[fan.ATTR_OSCILLATING] = False + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + await hass.services.async_call( + domain, service, data, blocking=False, context=context + ) + + response = directive.response() + response.add_context_property( + { + "namespace": "Alexa.ToggleController", + "instance": instance, + "name": "toggleState", + "value": "OFF", + } + ) + + return response + + +@HANDLERS.register(("Alexa.RangeController", "SetRangeValue")) +async def async_api_set_range(hass, config, directive, context): + """Process a next request.""" + entity = directive.entity + instance = directive.instance + domain = entity.domain + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + range_value = directive.payload["rangeValue"] + + # Fan Speed + if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + range_value = int(range_value) + service = fan.SERVICE_SET_SPEED + speed_list = entity.attributes[fan.ATTR_SPEED_LIST] + speed = next((v for i, v in enumerate(speed_list) if i == range_value), None) + + if not speed: + msg = "Entity does not support value" + raise AlexaInvalidValueError(msg) + + if speed == fan.SPEED_OFF: + service = fan.SERVICE_TURN_OFF + + data[fan.ATTR_SPEED] = speed + + # Cover Position + elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + range_value = int(range_value) + if range_value == 0: + service = cover.SERVICE_CLOSE_COVER + elif range_value == 100: + service = cover.SERVICE_OPEN_COVER + else: + service = cover.SERVICE_SET_COVER_POSITION + data[cover.ATTR_POSITION] = range_value + + # Cover Tilt + elif instance == f"{cover.DOMAIN}.tilt": + range_value = int(range_value) + if range_value == 0: + service = cover.SERVICE_CLOSE_COVER_TILT + elif range_value == 100: + service = cover.SERVICE_OPEN_COVER_TILT + else: + service = cover.SERVICE_SET_COVER_TILT_POSITION + data[cover.ATTR_TILT_POSITION] = range_value + + # Input Number Value + elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": + range_value = float(range_value) + service = input_number.SERVICE_SET_VALUE + min_value = float(entity.attributes[input_number.ATTR_MIN]) + max_value = float(entity.attributes[input_number.ATTR_MAX]) + data[input_number.ATTR_VALUE] = min(max_value, max(min_value, range_value)) + + # Vacuum Fan Speed + elif instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}": + service = vacuum.SERVICE_SET_FAN_SPEED + speed_list = entity.attributes[vacuum.ATTR_FAN_SPEED_LIST] + speed = next( + (v for i, v in enumerate(speed_list) if i == int(range_value)), None + ) + + if not speed: + msg = "Entity does not support value" + raise AlexaInvalidValueError(msg) + + data[vacuum.ATTR_FAN_SPEED] = speed + + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + await hass.services.async_call( + domain, service, data, blocking=False, context=context + ) + + response = directive.response() + response.add_context_property( + { + "namespace": "Alexa.RangeController", + "instance": instance, + "name": "rangeValue", + "value": range_value, + } + ) + + return response + + +@HANDLERS.register(("Alexa.RangeController", "AdjustRangeValue")) +async def async_api_adjust_range(hass, config, directive, context): + """Process a next request.""" + entity = directive.entity + instance = directive.instance + domain = entity.domain + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + range_delta = directive.payload["rangeValueDelta"] + range_delta_default = bool(directive.payload["rangeValueDeltaDefault"]) + response_value = 0 + + # Fan Speed + if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + range_delta = int(range_delta) + service = fan.SERVICE_SET_SPEED + speed_list = entity.attributes[fan.ATTR_SPEED_LIST] + current_speed = entity.attributes[fan.ATTR_SPEED] + current_speed_index = next( + (i for i, v in enumerate(speed_list) if v == current_speed), 0 + ) + new_speed_index = min( + len(speed_list) - 1, max(0, current_speed_index + range_delta) + ) + speed = next( + (v for i, v in enumerate(speed_list) if i == new_speed_index), None + ) + + if speed == fan.SPEED_OFF: + service = fan.SERVICE_TURN_OFF + + data[fan.ATTR_SPEED] = response_value = speed + + # Cover Position + elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + range_delta = int(range_delta * 20) if range_delta_default else int(range_delta) + service = SERVICE_SET_COVER_POSITION + current = entity.attributes.get(cover.ATTR_POSITION) + if not current: + msg = f"Unable to determine {entity.entity_id} current position" + raise AlexaInvalidValueError(msg) + position = response_value = min(100, max(0, range_delta + current)) + if position == 100: + service = cover.SERVICE_OPEN_COVER + elif position == 0: + service = cover.SERVICE_CLOSE_COVER + else: + data[cover.ATTR_POSITION] = position + + # Cover Tilt + elif instance == f"{cover.DOMAIN}.tilt": + range_delta = int(range_delta * 20) if range_delta_default else int(range_delta) + service = SERVICE_SET_COVER_TILT_POSITION + current = entity.attributes.get(cover.ATTR_TILT_POSITION) + if not current: + msg = f"Unable to determine {entity.entity_id} current tilt position" + raise AlexaInvalidValueError(msg) + tilt_position = response_value = min(100, max(0, range_delta + current)) + if tilt_position == 100: + service = cover.SERVICE_OPEN_COVER_TILT + elif tilt_position == 0: + service = cover.SERVICE_CLOSE_COVER_TILT + else: + data[cover.ATTR_TILT_POSITION] = tilt_position + + # Input Number Value + elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": + range_delta = float(range_delta) + service = input_number.SERVICE_SET_VALUE + min_value = float(entity.attributes[input_number.ATTR_MIN]) + max_value = float(entity.attributes[input_number.ATTR_MAX]) + current = float(entity.state) + data[input_number.ATTR_VALUE] = response_value = min( + max_value, max(min_value, range_delta + current) + ) + + # Vacuum Fan Speed + elif instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}": + range_delta = int(range_delta) + service = vacuum.SERVICE_SET_FAN_SPEED + speed_list = entity.attributes[vacuum.ATTR_FAN_SPEED_LIST] + current_speed = entity.attributes[vacuum.ATTR_FAN_SPEED] + current_speed_index = next( + (i for i, v in enumerate(speed_list) if v == current_speed), 0 + ) + new_speed_index = min( + len(speed_list) - 1, max(0, current_speed_index + range_delta) + ) + speed = next( + (v for i, v in enumerate(speed_list) if i == new_speed_index), None + ) + + data[vacuum.ATTR_FAN_SPEED] = response_value = speed + + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + await hass.services.async_call( + domain, service, data, blocking=False, context=context + ) + + response = directive.response() + response.add_context_property( + { + "namespace": "Alexa.RangeController", + "instance": instance, + "name": "rangeValue", + "value": response_value, + } + ) + + return response + + +@HANDLERS.register(("Alexa.ChannelController", "ChangeChannel")) +async def async_api_changechannel(hass, config, directive, context): + """Process a change channel request.""" + channel = "0" + entity = directive.entity + channel_payload = directive.payload["channel"] + metadata_payload = directive.payload["channelMetadata"] + payload_name = "number" + + if "number" in channel_payload: + channel = channel_payload["number"] + payload_name = "number" + elif "callSign" in channel_payload: + channel = channel_payload["callSign"] + payload_name = "callSign" + elif "affiliateCallSign" in channel_payload: + channel = channel_payload["affiliateCallSign"] + payload_name = "affiliateCallSign" + elif "uri" in channel_payload: + channel = channel_payload["uri"] + payload_name = "uri" + elif "name" in metadata_payload: + channel = metadata_payload["name"] + payload_name = "callSign" + + data = { + ATTR_ENTITY_ID: entity.entity_id, + media_player.const.ATTR_MEDIA_CONTENT_ID: channel, + media_player.const.ATTR_MEDIA_CONTENT_TYPE: media_player.const.MEDIA_TYPE_CHANNEL, + } + + await hass.services.async_call( + entity.domain, + media_player.const.SERVICE_PLAY_MEDIA, + data, + blocking=False, + context=context, + ) + + response = directive.response() + + response.add_context_property( + { + "namespace": "Alexa.ChannelController", + "name": "channel", + "value": {payload_name: channel}, + } + ) + + return response + + +@HANDLERS.register(("Alexa.ChannelController", "SkipChannels")) +async def async_api_skipchannel(hass, config, directive, context): + """Process a skipchannel request.""" + channel = int(directive.payload["channelCount"]) + entity = directive.entity + + data = {ATTR_ENTITY_ID: entity.entity_id} + + if channel < 0: + service_media = SERVICE_MEDIA_PREVIOUS_TRACK + else: + service_media = SERVICE_MEDIA_NEXT_TRACK + + for _ in range(abs(channel)): + await hass.services.async_call( + entity.domain, service_media, data, blocking=False, context=context + ) + + response = directive.response() + + response.add_context_property( + { + "namespace": "Alexa.ChannelController", + "name": "channel", + "value": {"number": ""}, + } + ) + + return response + + +@HANDLERS.register(("Alexa.SeekController", "AdjustSeekPosition")) +async def async_api_seek(hass, config, directive, context): + """Process a seek request.""" + entity = directive.entity + position_delta = int(directive.payload["deltaPositionMilliseconds"]) + + current_position = entity.attributes.get(media_player.ATTR_MEDIA_POSITION) + if not current_position: + msg = f"{entity} did not return the current media position." + raise AlexaVideoActionNotPermittedForContentError(msg) + + seek_position = max(int(current_position) + int(position_delta / 1000), 0) + + media_duration = entity.attributes.get(media_player.ATTR_MEDIA_DURATION) + if media_duration and 0 < int(media_duration) < seek_position: + seek_position = media_duration + + data = { + ATTR_ENTITY_ID: entity.entity_id, + media_player.ATTR_MEDIA_SEEK_POSITION: seek_position, + } + + await hass.services.async_call( + media_player.DOMAIN, + media_player.SERVICE_MEDIA_SEEK, + data, + blocking=False, + context=context, + ) + + # convert seconds to milliseconds for StateReport. + seek_position = int(seek_position * 1000) + + payload = {"properties": [{"name": "positionMilliseconds", "value": seek_position}]} + return directive.response( + name="StateReport", namespace="Alexa.SeekController", payload=payload + ) + + +@HANDLERS.register(("Alexa.EqualizerController", "SetMode")) +async def async_api_set_eq_mode(hass, config, directive, context): + """Process a SetMode request for EqualizerController.""" + mode = directive.payload["mode"] + entity = directive.entity + data = {ATTR_ENTITY_ID: entity.entity_id} + + sound_mode_list = entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST) + if sound_mode_list and mode.lower() in sound_mode_list: + data[media_player.const.ATTR_SOUND_MODE] = mode.lower() + else: + msg = f"failed to map sound mode {mode} to a mode on {entity.entity_id}" + raise AlexaInvalidValueError(msg) + + await hass.services.async_call( + entity.domain, + media_player.SERVICE_SELECT_SOUND_MODE, + data, + blocking=False, + context=context, + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.EqualizerController", "AdjustBands")) +@HANDLERS.register(("Alexa.EqualizerController", "ResetBands")) +@HANDLERS.register(("Alexa.EqualizerController", "SetBands")) +async def async_api_bands_directive(hass, config, directive, context): + """Handle an AdjustBands, ResetBands, SetBands request. + + Only mode directives are currently supported for the EqualizerController. + """ + # Currently bands directives are not supported. + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + +@HANDLERS.register(("Alexa.TimeHoldController", "Hold")) +async def async_api_hold(hass, config, directive, context): + """Process a TimeHoldController Hold request.""" + entity = directive.entity + data = {ATTR_ENTITY_ID: entity.entity_id} + + if entity.domain == timer.DOMAIN: + service = timer.SERVICE_PAUSE + + elif entity.domain == vacuum.DOMAIN: + service = vacuum.SERVICE_START_PAUSE + + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + await hass.services.async_call( + entity.domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.TimeHoldController", "Resume")) +async def async_api_resume(hass, config, directive, context): + """Process a TimeHoldController Resume request.""" + entity = directive.entity + data = {ATTR_ENTITY_ID: entity.entity_id} + + if entity.domain == timer.DOMAIN: + service = timer.SERVICE_START + + elif entity.domain == vacuum.DOMAIN: + service = vacuum.SERVICE_START_PAUSE + + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + await hass.services.async_call( + entity.domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.CameraStreamController", "InitializeCameraStreams")) +async def async_api_initialize_camera_stream(hass, config, directive, context): + """Process a InitializeCameraStreams request.""" + entity = directive.entity + stream_source = await camera.async_request_stream(hass, entity.entity_id, fmt="hls") + camera_image = hass.states.get(entity.entity_id).attributes[ATTR_ENTITY_PICTURE] + + try: + external_url = network.get_url( + hass, + allow_internal=False, + allow_ip=False, + require_ssl=True, + require_standard_port=True, + ) + except network.NoURLAvailableError as err: + raise AlexaInvalidValueError( + "Failed to find suitable URL to serve to Alexa" + ) from err + + payload = { + "cameraStreams": [ + { + "uri": f"{external_url}{stream_source}", + "protocol": "HLS", + "resolution": {"width": 1280, "height": 720}, + "authorizationType": "NONE", + "videoCodec": "H264", + "audioCodec": "AAC", + } + ], + "imageUri": f"{external_url}{camera_image}", + } + return directive.response( + name="Response", namespace="Alexa.CameraStreamController", payload=payload + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/intent.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/intent.py new file mode 100644 index 00000000000..f64031250e2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/intent.py @@ -0,0 +1,294 @@ +"""Support for Alexa skill service end point.""" +import enum +import logging + +from homeassistant.components import http +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import intent +from homeassistant.util.decorator import Registry + +from .const import DOMAIN, SYN_RESOLUTION_MATCH + +_LOGGER = logging.getLogger(__name__) + +HANDLERS = Registry() + +INTENTS_API_ENDPOINT = "/api/alexa" + + +class SpeechType(enum.Enum): + """The Alexa speech types.""" + + plaintext = "PlainText" + ssml = "SSML" + + +SPEECH_MAPPINGS = {"plain": SpeechType.plaintext, "ssml": SpeechType.ssml} + + +class CardType(enum.Enum): + """The Alexa card types.""" + + simple = "Simple" + link_account = "LinkAccount" + + +@callback +def async_setup(hass): + """Activate Alexa component.""" + hass.http.register_view(AlexaIntentsView) + + +async def async_setup_intents(hass): + """ + Do intents setup. + + Right now this module does not expose any, but the intent component breaks + without it. + """ + pass # pylint: disable=unnecessary-pass + + +class UnknownRequest(HomeAssistantError): + """When an unknown Alexa request is passed in.""" + + +class AlexaIntentsView(http.HomeAssistantView): + """Handle Alexa requests.""" + + url = INTENTS_API_ENDPOINT + name = "api:alexa" + + async def post(self, request): + """Handle Alexa.""" + hass = request.app["hass"] + message = await request.json() + + _LOGGER.debug("Received Alexa request: %s", message) + + try: + response = await async_handle_message(hass, message) + return b"" if response is None else self.json(response) + except UnknownRequest as err: + _LOGGER.warning(str(err)) + return self.json(intent_error_response(hass, message, str(err))) + + except intent.UnknownIntent as err: + _LOGGER.warning(str(err)) + return self.json( + intent_error_response( + hass, + message, + "This intent is not yet configured within Home Assistant.", + ) + ) + + except intent.InvalidSlotInfo as err: + _LOGGER.error("Received invalid slot data from Alexa: %s", err) + return self.json( + intent_error_response( + hass, message, "Invalid slot information received for this intent." + ) + ) + + except intent.IntentError as err: + _LOGGER.exception(str(err)) + return self.json( + intent_error_response(hass, message, "Error handling intent.") + ) + + +def intent_error_response(hass, message, error): + """Return an Alexa response that will speak the error message.""" + alexa_intent_info = message.get("request").get("intent") + alexa_response = AlexaResponse(hass, alexa_intent_info) + alexa_response.add_speech(SpeechType.plaintext, error) + return alexa_response.as_dict() + + +async def async_handle_message(hass, message): + """Handle an Alexa intent. + + Raises: + - UnknownRequest + - intent.UnknownIntent + - intent.InvalidSlotInfo + - intent.IntentError + + """ + req = message.get("request") + req_type = req["type"] + + handler = HANDLERS.get(req_type) + + if not handler: + raise UnknownRequest(f"Received unknown request {req_type}") + + return await handler(hass, message) + + +@HANDLERS.register("SessionEndedRequest") +async def async_handle_session_end(hass, message): + """Handle a session end request.""" + return None + + +@HANDLERS.register("IntentRequest") +@HANDLERS.register("LaunchRequest") +async def async_handle_intent(hass, message): + """Handle an intent request. + + Raises: + - intent.UnknownIntent + - intent.InvalidSlotInfo + - intent.IntentError + + """ + req = message.get("request") + alexa_intent_info = req.get("intent") + alexa_response = AlexaResponse(hass, alexa_intent_info) + + if req["type"] == "LaunchRequest": + intent_name = ( + message.get("session", {}).get("application", {}).get("applicationId") + ) + else: + intent_name = alexa_intent_info["name"] + + intent_response = await intent.async_handle( + hass, + DOMAIN, + intent_name, + {key: {"value": value} for key, value in alexa_response.variables.items()}, + ) + + for intent_speech, alexa_speech in SPEECH_MAPPINGS.items(): + if intent_speech in intent_response.speech: + alexa_response.add_speech( + alexa_speech, intent_response.speech[intent_speech]["speech"] + ) + break + + if "simple" in intent_response.card: + alexa_response.add_card( + CardType.simple, + intent_response.card["simple"]["title"], + intent_response.card["simple"]["content"], + ) + + return alexa_response.as_dict() + + +def resolve_slot_synonyms(key, request): + """Check slot request for synonym resolutions.""" + # Default to the spoken slot value if more than one or none are found. For + # reference to the request object structure, see the Alexa docs: + # https://tinyurl.com/ybvm7jhs + resolved_value = request["value"] + + if ( + "resolutions" in request + and "resolutionsPerAuthority" in request["resolutions"] + and len(request["resolutions"]["resolutionsPerAuthority"]) >= 1 + ): + + # Extract all of the possible values from each authority with a + # successful match + possible_values = [] + + for entry in request["resolutions"]["resolutionsPerAuthority"]: + if entry["status"]["code"] != SYN_RESOLUTION_MATCH: + continue + + possible_values.extend([item["value"]["name"] for item in entry["values"]]) + + # If there is only one match use the resolved value, otherwise the + # resolution cannot be determined, so use the spoken slot value + if len(possible_values) == 1: + resolved_value = possible_values[0] + else: + _LOGGER.debug( + "Found multiple synonym resolutions for slot value: {%s: %s}", + key, + resolved_value, + ) + + return resolved_value + + +class AlexaResponse: + """Help generating the response for Alexa.""" + + def __init__(self, hass, intent_info): + """Initialize the response.""" + self.hass = hass + self.speech = None + self.card = None + self.reprompt = None + self.session_attributes = {} + self.should_end_session = True + self.variables = {} + + # Intent is None if request was a LaunchRequest or SessionEndedRequest + if intent_info is not None: + for key, value in intent_info.get("slots", {}).items(): + # Only include slots with values + if "value" not in value: + continue + + _key = key.replace(".", "_") + + self.variables[_key] = resolve_slot_synonyms(key, value) + + def add_card(self, card_type, title, content): + """Add a card to the response.""" + assert self.card is None + + card = {"type": card_type.value} + + if card_type == CardType.link_account: + self.card = card + return + + card["title"] = title + card["content"] = content + self.card = card + + def add_speech(self, speech_type, text): + """Add speech to the response.""" + assert self.speech is None + + key = "ssml" if speech_type == SpeechType.ssml else "text" + + self.speech = {"type": speech_type.value, key: text} + + def add_reprompt(self, speech_type, text): + """Add reprompt if user does not answer.""" + assert self.reprompt is None + + key = "ssml" if speech_type == SpeechType.ssml else "text" + + self.reprompt = { + "type": speech_type.value, + key: text.async_render(self.variables, parse_result=False), + } + + def as_dict(self): + """Return response in an Alexa valid dict.""" + response = {"shouldEndSession": self.should_end_session} + + if self.card is not None: + response["card"] = self.card + + if self.speech is not None: + response["outputSpeech"] = self.speech + + if self.reprompt is not None: + response["reprompt"] = {"outputSpeech": self.reprompt} + + return { + "version": "1.0", + "sessionAttributes": self.session_attributes, + "response": response, + } diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/logbook.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/logbook.py new file mode 100644 index 00000000000..153c7b7d61a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/logbook.py @@ -0,0 +1,28 @@ +"""Describe logbook events.""" +from homeassistant.core import callback + +from .const import DOMAIN, EVENT_ALEXA_SMART_HOME + + +@callback +def async_describe_events(hass, async_describe_event): + """Describe logbook events.""" + + @callback + def async_describe_logbook_event(event): + """Describe a logbook event.""" + data = event.data + entity_id = data["request"].get("entity_id") + + if entity_id: + state = hass.states.get(entity_id) + name = state.name if state else entity_id + message = f"sent command {data['request']['namespace']}/{data['request']['name']} for {name}" + else: + message = ( + f"sent command {data['request']['namespace']}/{data['request']['name']}" + ) + + return {"name": "Amazon Alexa", "message": message, "entity_id": entity_id} + + async_describe_event(DOMAIN, EVENT_ALEXA_SMART_HOME, async_describe_logbook_event) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/manifest.json new file mode 100644 index 00000000000..486079b0313 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "alexa", + "name": "Amazon Alexa", + "documentation": "https://www.home-assistant.io/integrations/alexa", + "dependencies": ["http"], + "after_dependencies": ["camera"], + "codeowners": ["@home-assistant/cloud", "@ochlocracy"], + "iot_class": "cloud_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/messages.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/messages.py new file mode 100644 index 00000000000..4dd154ea11f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/messages.py @@ -0,0 +1,195 @@ +"""Alexa models.""" +import logging +from uuid import uuid4 + +from .const import ( + API_CONTEXT, + API_DIRECTIVE, + API_ENDPOINT, + API_EVENT, + API_HEADER, + API_PAYLOAD, + API_SCOPE, +) +from .entities import ENTITY_ADAPTERS +from .errors import AlexaInvalidEndpointError + +_LOGGER = logging.getLogger(__name__) + + +class AlexaDirective: + """An incoming Alexa directive.""" + + def __init__(self, request): + """Initialize a directive.""" + self._directive = request[API_DIRECTIVE] + self.namespace = self._directive[API_HEADER]["namespace"] + self.name = self._directive[API_HEADER]["name"] + self.payload = self._directive[API_PAYLOAD] + self.has_endpoint = API_ENDPOINT in self._directive + + self.entity = self.entity_id = self.endpoint = self.instance = None + + def load_entity(self, hass, config): + """Set attributes related to the entity for this request. + + Sets these attributes when self.has_endpoint is True: + + - entity + - entity_id + - endpoint + - instance (when header includes instance property) + + Behavior when self.has_endpoint is False is undefined. + + Will raise AlexaInvalidEndpointError if the endpoint in the request is + malformed or nonexistent. + """ + _endpoint_id = self._directive[API_ENDPOINT]["endpointId"] + self.entity_id = _endpoint_id.replace("#", ".") + + self.entity = hass.states.get(self.entity_id) + if not self.entity or not config.should_expose(self.entity_id): + raise AlexaInvalidEndpointError(_endpoint_id) + + self.endpoint = ENTITY_ADAPTERS[self.entity.domain](hass, config, self.entity) + if "instance" in self._directive[API_HEADER]: + self.instance = self._directive[API_HEADER]["instance"] + + def response(self, name="Response", namespace="Alexa", payload=None): + """Create an API formatted response. + + Async friendly. + """ + response = AlexaResponse(name, namespace, payload) + + token = self._directive[API_HEADER].get("correlationToken") + if token: + response.set_correlation_token(token) + + if self.has_endpoint: + response.set_endpoint(self._directive[API_ENDPOINT].copy()) + + return response + + def error( + self, + namespace="Alexa", + error_type="INTERNAL_ERROR", + error_message="", + payload=None, + ): + """Create a API formatted error response. + + Async friendly. + """ + payload = payload or {} + payload["type"] = error_type + payload["message"] = error_message + + _LOGGER.info( + "Request %s/%s error %s: %s", + self._directive[API_HEADER]["namespace"], + self._directive[API_HEADER]["name"], + error_type, + error_message, + ) + + return self.response(name="ErrorResponse", namespace=namespace, payload=payload) + + +class AlexaResponse: + """Class to hold a response.""" + + def __init__(self, name, namespace, payload=None): + """Initialize the response.""" + payload = payload or {} + self._response = { + API_EVENT: { + API_HEADER: { + "namespace": namespace, + "name": name, + "messageId": str(uuid4()), + "payloadVersion": "3", + }, + API_PAYLOAD: payload, + } + } + + @property + def name(self): + """Return the name of this response.""" + return self._response[API_EVENT][API_HEADER]["name"] + + @property + def namespace(self): + """Return the namespace of this response.""" + return self._response[API_EVENT][API_HEADER]["namespace"] + + def set_correlation_token(self, token): + """Set the correlationToken. + + This should normally mirror the value from a request, and is set by + AlexaDirective.response() usually. + """ + self._response[API_EVENT][API_HEADER]["correlationToken"] = token + + def set_endpoint_full(self, bearer_token, endpoint_id, cookie=None): + """Set the endpoint dictionary. + + This is used to send proactive messages to Alexa. + """ + self._response[API_EVENT][API_ENDPOINT] = { + API_SCOPE: {"type": "BearerToken", "token": bearer_token} + } + + if endpoint_id is not None: + self._response[API_EVENT][API_ENDPOINT]["endpointId"] = endpoint_id + + if cookie is not None: + self._response[API_EVENT][API_ENDPOINT]["cookie"] = cookie + + def set_endpoint(self, endpoint): + """Set the endpoint. + + This should normally mirror the value from a request, and is set by + AlexaDirective.response() usually. + """ + self._response[API_EVENT][API_ENDPOINT] = endpoint + + def _properties(self): + context = self._response.setdefault(API_CONTEXT, {}) + return context.setdefault("properties", []) + + def add_context_property(self, prop): + """Add a property to the response context. + + The Alexa response includes a list of properties which provides + feedback on how states have changed. For example if a user asks, + "Alexa, set thermostat to 20 degrees", the API expects a response with + the new value of the property, and Alexa will respond to the user + "Thermostat set to 20 degrees". + + async_handle_message() will call .merge_context_properties() for every + request automatically, however often handlers will call services to + change state but the effects of those changes are applied + asynchronously. Thus, handlers should call this method to confirm + changes before returning. + """ + self._properties().append(prop) + + def merge_context_properties(self, endpoint): + """Add all properties from given endpoint if not already set. + + Handlers should be using .add_context_property(). + """ + properties = self._properties() + already_set = {(p["namespace"], p["name"]) for p in properties} + + for prop in endpoint.serialize_properties(): + if (prop["namespace"], prop["name"]) not in already_set: + self.add_context_property(prop) + + def serialize(self): + """Return response as a JSON-able data structure.""" + return self._response diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/resources.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/resources.py new file mode 100644 index 00000000000..5c02eca4fb2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/resources.py @@ -0,0 +1,399 @@ +"""Alexa Resources and Assets.""" + + +class AlexaGlobalCatalog: + """The Global Alexa catalog. + + https://developer.amazon.com/docs/device-apis/resources-and-assets.html#global-alexa-catalog + + You can use the global Alexa catalog for pre-defined names of devices, settings, values, and units. + This catalog is localized into all the languages that Alexa supports. + + You can reference the following catalog of pre-defined friendly names. + Each item in the following list is an asset identifier followed by its supported friendly names. + The first friendly name for each identifier is the one displayed in the Alexa mobile app. + """ + + # Air Purifier, Air Cleaner,Clean Air Machine + DEVICE_NAME_AIR_PURIFIER = "Alexa.DeviceName.AirPurifier" + + # Fan, Blower + DEVICE_NAME_FAN = "Alexa.DeviceName.Fan" + + # Router, Internet Router, Network Router, Wifi Router, Net Router + DEVICE_NAME_ROUTER = "Alexa.DeviceName.Router" + + # Shade, Blind, Curtain, Roller, Shutter, Drape, Awning, Window shade, Interior blind + DEVICE_NAME_SHADE = "Alexa.DeviceName.Shade" + + # Shower + DEVICE_NAME_SHOWER = "Alexa.DeviceName.Shower" + + # Space Heater, Portable Heater + DEVICE_NAME_SPACE_HEATER = "Alexa.DeviceName.SpaceHeater" + + # Washer, Washing Machine + DEVICE_NAME_WASHER = "Alexa.DeviceName.Washer" + + # 2.4G Guest Wi-Fi, 2.4G Guest Network, Guest Network 2.4G, 2G Guest Wifi + SETTING_2G_GUEST_WIFI = "Alexa.Setting.2GGuestWiFi" + + # 5G Guest Wi-Fi, 5G Guest Network, Guest Network 5G, 5G Guest Wifi + SETTING_5G_GUEST_WIFI = "Alexa.Setting.5GGuestWiFi" + + # Auto, Automatic, Automatic Mode, Auto Mode + SETTING_AUTO = "Alexa.Setting.Auto" + + # Direction + SETTING_DIRECTION = "Alexa.Setting.Direction" + + # Dry Cycle, Dry Preset, Dry Setting, Dryer Cycle, Dryer Preset, Dryer Setting + SETTING_DRY_CYCLE = "Alexa.Setting.DryCycle" + + # Fan Speed, Airflow speed, Wind Speed, Air speed, Air velocity + SETTING_FAN_SPEED = "Alexa.Setting.FanSpeed" + + # Guest Wi-fi, Guest Network, Guest Net + SETTING_GUEST_WIFI = "Alexa.Setting.GuestWiFi" + + # Heat + SETTING_HEAT = "Alexa.Setting.Heat" + + # Mode + SETTING_MODE = "Alexa.Setting.Mode" + + # Night, Night Mode + SETTING_NIGHT = "Alexa.Setting.Night" + + # Opening, Height, Lift, Width + SETTING_OPENING = "Alexa.Setting.Opening" + + # Oscillate, Swivel, Oscillation, Spin, Back and forth + SETTING_OSCILLATE = "Alexa.Setting.Oscillate" + + # Preset, Setting + SETTING_PRESET = "Alexa.Setting.Preset" + + # Quiet, Quiet Mode, Noiseless, Silent + SETTING_QUIET = "Alexa.Setting.Quiet" + + # Temperature, Temp + SETTING_TEMPERATURE = "Alexa.Setting.Temperature" + + # Wash Cycle, Wash Preset, Wash setting + SETTING_WASH_CYCLE = "Alexa.Setting.WashCycle" + + # Water Temperature, Water Temp, Water Heat + SETTING_WATER_TEMPERATURE = "Alexa.Setting.WaterTemperature" + + # Handheld Shower, Shower Wand, Hand Shower + SHOWER_HAND_HELD = "Alexa.Shower.HandHeld" + + # Rain Head, Overhead shower, Rain Shower, Rain Spout, Rain Faucet + SHOWER_RAIN_HEAD = "Alexa.Shower.RainHead" + + # Degrees, Degree + UNIT_ANGLE_DEGREES = "Alexa.Unit.Angle.Degrees" + + # Radians, Radian + UNIT_ANGLE_RADIANS = "Alexa.Unit.Angle.Radians" + + # Feet, Foot + UNIT_DISTANCE_FEET = "Alexa.Unit.Distance.Feet" + + # Inches, Inch + UNIT_DISTANCE_INCHES = "Alexa.Unit.Distance.Inches" + + # Kilometers + UNIT_DISTANCE_KILOMETERS = "Alexa.Unit.Distance.Kilometers" + + # Meters, Meter, m + UNIT_DISTANCE_METERS = "Alexa.Unit.Distance.Meters" + + # Miles, Mile + UNIT_DISTANCE_MILES = "Alexa.Unit.Distance.Miles" + + # Yards, Yard + UNIT_DISTANCE_YARDS = "Alexa.Unit.Distance.Yards" + + # Grams, Gram, g + UNIT_MASS_GRAMS = "Alexa.Unit.Mass.Grams" + + # Kilograms, Kilogram, kg + UNIT_MASS_KILOGRAMS = "Alexa.Unit.Mass.Kilograms" + + # Percent + UNIT_PERCENT = "Alexa.Unit.Percent" + + # Celsius, Degrees Celsius, Degrees, C, Centigrade, Degrees Centigrade + UNIT_TEMPERATURE_CELSIUS = "Alexa.Unit.Temperature.Celsius" + + # Degrees, Degree + UNIT_TEMPERATURE_DEGREES = "Alexa.Unit.Temperature.Degrees" + + # Fahrenheit, Degrees Fahrenheit, Degrees F, Degrees, F + UNIT_TEMPERATURE_FAHRENHEIT = "Alexa.Unit.Temperature.Fahrenheit" + + # Kelvin, Degrees Kelvin, Degrees K, Degrees, K + UNIT_TEMPERATURE_KELVIN = "Alexa.Unit.Temperature.Kelvin" + + # Cubic Feet, Cubic Foot + UNIT_VOLUME_CUBIC_FEET = "Alexa.Unit.Volume.CubicFeet" + + # Cubic Meters, Cubic Meter, Meters Cubed + UNIT_VOLUME_CUBIC_METERS = "Alexa.Unit.Volume.CubicMeters" + + # Gallons, Gallon + UNIT_VOLUME_GALLONS = "Alexa.Unit.Volume.Gallons" + + # Liters, Liter, L + UNIT_VOLUME_LITERS = "Alexa.Unit.Volume.Liters" + + # Pints, Pint + UNIT_VOLUME_PINTS = "Alexa.Unit.Volume.Pints" + + # Quarts, Quart + UNIT_VOLUME_QUARTS = "Alexa.Unit.Volume.Quarts" + + # Ounces, Ounce, oz + UNIT_WEIGHT_OUNCES = "Alexa.Unit.Weight.Ounces" + + # Pounds, Pound, lbs + UNIT_WEIGHT_POUNDS = "Alexa.Unit.Weight.Pounds" + + # Close + VALUE_CLOSE = "Alexa.Value.Close" + + # Delicates, Delicate + VALUE_DELICATE = "Alexa.Value.Delicate" + + # High + VALUE_HIGH = "Alexa.Value.High" + + # Low + VALUE_LOW = "Alexa.Value.Low" + + # Maximum, Max + VALUE_MAXIMUM = "Alexa.Value.Maximum" + + # Medium, Mid + VALUE_MEDIUM = "Alexa.Value.Medium" + + # Minimum, Min + VALUE_MINIMUM = "Alexa.Value.Minimum" + + # Open + VALUE_OPEN = "Alexa.Value.Open" + + # Quick Wash, Fast Wash, Wash Quickly, Speed Wash + VALUE_QUICK_WASH = "Alexa.Value.QuickWash" + + +class AlexaCapabilityResource: + """Base class for Alexa capabilityResources, modeResources, and presetResources objects. + + Resources objects labels must be unique across all modeResources and presetResources within the same device. + To provide support for all supported locales, include one label from the AlexaGlobalCatalog in the labels array. + You cannot use any words from the following list as friendly names: + https://developer.amazon.com/docs/alexa/device-apis/resources-and-assets.html#names-you-cannot-use + + https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources + """ + + def __init__(self, labels): + """Initialize an Alexa resource.""" + self._resource_labels = [] + for label in labels: + self._resource_labels.append(label) + + def serialize_capability_resources(self): + """Return capabilityResources object serialized for an API response.""" + return self.serialize_labels(self._resource_labels) + + @staticmethod + def serialize_configuration(): + """Return ModeResources, PresetResources friendlyNames serialized for an API response.""" + return [] + + @staticmethod + def serialize_labels(resources): + """Return resource label objects for friendlyNames serialized for an API response.""" + labels = [] + for label in resources: + if label in AlexaGlobalCatalog.__dict__.values(): + label = {"@type": "asset", "value": {"assetId": label}} + else: + label = {"@type": "text", "value": {"text": label, "locale": "en-US"}} + + labels.append(label) + + return {"friendlyNames": labels} + + +class AlexaModeResource(AlexaCapabilityResource): + """Implements Alexa ModeResources. + + https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources + """ + + def __init__(self, labels, ordered=False): + """Initialize an Alexa modeResource.""" + super().__init__(labels) + self._supported_modes = [] + self._mode_ordered = ordered + + def add_mode(self, value, labels): + """Add mode to the supportedModes object.""" + self._supported_modes.append({"value": value, "labels": labels}) + + def serialize_configuration(self): + """Return configuration for ModeResources friendlyNames serialized for an API response.""" + mode_resources = [] + for mode in self._supported_modes: + result = { + "value": mode["value"], + "modeResources": self.serialize_labels(mode["labels"]), + } + mode_resources.append(result) + + return {"ordered": self._mode_ordered, "supportedModes": mode_resources} + + +class AlexaPresetResource(AlexaCapabilityResource): + """Implements Alexa PresetResources. + + Use presetResources with RangeController to provide a set of friendlyNames for each RangeController preset. + + https://developer.amazon.com/docs/device-apis/resources-and-assets.html#presetresources + """ + + def __init__(self, labels, min_value, max_value, precision, unit=None): + """Initialize an Alexa presetResource.""" + super().__init__(labels) + self._presets = [] + self._minimum_value = min_value + self._maximum_value = max_value + self._precision = precision + self._unit_of_measure = None + if unit in AlexaGlobalCatalog.__dict__.values(): + self._unit_of_measure = unit + + def add_preset(self, value, labels): + """Add preset to configuration presets array.""" + self._presets.append({"value": value, "labels": labels}) + + def serialize_configuration(self): + """Return configuration for PresetResources friendlyNames serialized for an API response.""" + configuration = { + "supportedRange": { + "minimumValue": self._minimum_value, + "maximumValue": self._maximum_value, + "precision": self._precision, + } + } + + if self._unit_of_measure: + configuration["unitOfMeasure"] = self._unit_of_measure + + if self._presets: + preset_resources = [ + { + "rangeValue": preset["value"], + "presetResources": self.serialize_labels(preset["labels"]), + } + for preset in self._presets + ] + configuration["presets"] = preset_resources + + return configuration + + +class AlexaSemantics: + """Class for Alexa Semantics Object. + + You can optionally enable additional utterances by using semantics. When you use semantics, + you manually map the phrases "open", "close", "raise", and "lower" to directives. + + Semantics is supported for the following interfaces only: ModeController, RangeController, and ToggleController. + + Semantics stateMappings are only supported for one interface of the same type on the same device. If a device has + multiple RangeControllers only one interface may use stateMappings otherwise discovery will fail. + + You can support semantics actionMappings on different controllers for the same device, however each controller must + support different phrases. For example, you can support "raise" on a RangeController, and "open" on a ModeController, + but you can't support "open" on both RangeController and ModeController. Semantics stateMappings are only supported + for one interface on the same device. + + https://developer.amazon.com/docs/device-apis/alexa-discovery.html#semantics-object + """ + + MAPPINGS_ACTION = "actionMappings" + MAPPINGS_STATE = "stateMappings" + + ACTIONS_TO_DIRECTIVE = "ActionsToDirective" + STATES_TO_VALUE = "StatesToValue" + STATES_TO_RANGE = "StatesToRange" + + ACTION_CLOSE = "Alexa.Actions.Close" + ACTION_LOWER = "Alexa.Actions.Lower" + ACTION_OPEN = "Alexa.Actions.Open" + ACTION_RAISE = "Alexa.Actions.Raise" + + STATES_OPEN = "Alexa.States.Open" + STATES_CLOSED = "Alexa.States.Closed" + + DIRECTIVE_RANGE_SET_VALUE = "SetRangeValue" + DIRECTIVE_RANGE_ADJUST_VALUE = "AdjustRangeValue" + DIRECTIVE_TOGGLE_TURN_ON = "TurnOn" + DIRECTIVE_TOGGLE_TURN_OFF = "TurnOff" + DIRECTIVE_MODE_SET_MODE = "SetMode" + DIRECTIVE_MODE_ADJUST_MODE = "AdjustMode" + + def __init__(self): + """Initialize an Alexa modeResource.""" + self._action_mappings = [] + self._state_mappings = [] + + def _add_action_mapping(self, semantics): + """Add action mapping between actions and interface directives.""" + self._action_mappings.append(semantics) + + def _add_state_mapping(self, semantics): + """Add state mapping between states and interface directives.""" + self._state_mappings.append(semantics) + + def add_states_to_value(self, states, value): + """Add StatesToValue stateMappings.""" + self._add_state_mapping( + {"@type": self.STATES_TO_VALUE, "states": states, "value": value} + ) + + def add_states_to_range(self, states, min_value, max_value): + """Add StatesToRange stateMappings.""" + self._add_state_mapping( + { + "@type": self.STATES_TO_RANGE, + "states": states, + "range": {"minimumValue": min_value, "maximumValue": max_value}, + } + ) + + def add_action_to_directive(self, actions, directive, payload): + """Add ActionsToDirective actionMappings.""" + self._add_action_mapping( + { + "@type": self.ACTIONS_TO_DIRECTIVE, + "actions": actions, + "directive": {"name": directive, "payload": payload}, + } + ) + + def serialize_semantics(self): + """Return semantics object serialized for an API response.""" + semantics = {} + if self._action_mappings: + semantics[self.MAPPINGS_ACTION] = self._action_mappings + if self._state_mappings: + semantics[self.MAPPINGS_STATE] = self._state_mappings + + return semantics diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/services.yaml b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/smart_home.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/smart_home.py new file mode 100644 index 00000000000..0f166ab3a27 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/smart_home.py @@ -0,0 +1,66 @@ +"""Support for alexa Smart Home Skill API.""" +import logging + +import homeassistant.core as ha + +from .const import API_DIRECTIVE, API_HEADER, EVENT_ALEXA_SMART_HOME +from .errors import AlexaBridgeUnreachableError, AlexaError +from .handlers import HANDLERS +from .messages import AlexaDirective + +_LOGGER = logging.getLogger(__name__) + + +async def async_handle_message(hass, config, request, context=None, enabled=True): + """Handle incoming API messages. + + If enabled is False, the response to all messagess will be a + BRIDGE_UNREACHABLE error. This can be used if the API has been disabled in + configuration. + """ + assert request[API_DIRECTIVE][API_HEADER]["payloadVersion"] == "3" + + if context is None: + context = ha.Context() + + directive = AlexaDirective(request) + + try: + if not enabled: + raise AlexaBridgeUnreachableError( + "Alexa API not enabled in Home Assistant configuration" + ) + + if directive.has_endpoint: + directive.load_entity(hass, config) + + funct_ref = HANDLERS.get((directive.namespace, directive.name)) + if funct_ref: + response = await funct_ref(hass, config, directive, context) + if directive.has_endpoint: + response.merge_context_properties(directive.endpoint) + else: + _LOGGER.warning( + "Unsupported API request %s/%s", directive.namespace, directive.name + ) + response = directive.error() + except AlexaError as err: + response = directive.error( + error_type=err.error_type, error_message=err.error_message + ) + + request_info = {"namespace": directive.namespace, "name": directive.name} + + if directive.has_endpoint: + request_info["entity_id"] = directive.entity_id + + hass.bus.async_fire( + EVENT_ALEXA_SMART_HOME, + { + "request": request_info, + "response": {"namespace": response.namespace, "name": response.name}, + }, + context=context, + ) + + return response.serialize() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/smart_home_http.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/smart_home_http.py new file mode 100644 index 00000000000..41738c824fb --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/smart_home_http.py @@ -0,0 +1,122 @@ +"""Alexa HTTP interface.""" +import logging + +from homeassistant import core +from homeassistant.components.http.view import HomeAssistantView +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET + +from .auth import Auth +from .config import AbstractConfig +from .const import CONF_ENDPOINT, CONF_ENTITY_CONFIG, CONF_FILTER, CONF_LOCALE +from .smart_home import async_handle_message +from .state_report import async_enable_proactive_mode + +_LOGGER = logging.getLogger(__name__) +SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home" + + +class AlexaConfig(AbstractConfig): + """Alexa config.""" + + def __init__(self, hass, config): + """Initialize Alexa config.""" + super().__init__(hass) + self._config = config + + if config.get(CONF_CLIENT_ID) and config.get(CONF_CLIENT_SECRET): + self._auth = Auth(hass, config[CONF_CLIENT_ID], config[CONF_CLIENT_SECRET]) + else: + self._auth = None + + @property + def supports_auth(self): + """Return if config supports auth.""" + return self._auth is not None + + @property + def should_report_state(self): + """Return if we should proactively report states.""" + return self._auth is not None + + @property + def endpoint(self): + """Endpoint for report state.""" + return self._config.get(CONF_ENDPOINT) + + @property + def entity_config(self): + """Return entity config.""" + return self._config.get(CONF_ENTITY_CONFIG) or {} + + @property + def locale(self): + """Return config locale.""" + return self._config.get(CONF_LOCALE) + + @core.callback + def user_identifier(self): + """Return an identifier for the user that represents this config.""" + return "" + + def should_expose(self, entity_id): + """If an entity should be exposed.""" + return self._config[CONF_FILTER](entity_id) + + @core.callback + def async_invalidate_access_token(self): + """Invalidate access token.""" + self._auth.async_invalidate_access_token() + + async def async_get_access_token(self): + """Get an access token.""" + return await self._auth.async_get_access_token() + + async def async_accept_grant(self, code): + """Accept a grant.""" + return await self._auth.async_do_auth(code) + + +async def async_setup(hass, config): + """Activate Smart Home functionality of Alexa component. + + This is optional, triggered by having a `smart_home:` sub-section in the + alexa configuration. + + Even if that's disabled, the functionality in this module may still be used + by the cloud component which will call async_handle_message directly. + """ + smart_home_config = AlexaConfig(hass, config) + hass.http.register_view(SmartHomeView(smart_home_config)) + + if smart_home_config.should_report_state: + await async_enable_proactive_mode(hass, smart_home_config) + + +class SmartHomeView(HomeAssistantView): + """Expose Smart Home v3 payload interface via HTTP POST.""" + + url = SMART_HOME_HTTP_ENDPOINT + name = "api:alexa:smart_home" + + def __init__(self, smart_home_config): + """Initialize.""" + self.smart_home_config = smart_home_config + + async def post(self, request): + """Handle Alexa Smart Home requests. + + The Smart Home API requires the endpoint to be implemented in AWS + Lambda, which will need to forward the requests to here and pass back + the response. + """ + hass = request.app["hass"] + user = request["hass_user"] + message = await request.json() + + _LOGGER.debug("Received Alexa Smart Home request: %s", message) + + response = await async_handle_message( + hass, self.smart_home_config, message, context=core.Context(user_id=user.id) + ) + _LOGGER.debug("Sending Alexa Smart Home response: %s", response) + return b"" if response is None else self.json(response) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/state_report.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/state_report.py new file mode 100644 index 00000000000..712a08ac6b9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alexa/state_report.py @@ -0,0 +1,291 @@ +"""Alexa state report code.""" +from __future__ import annotations + +import asyncio +import json +import logging + +import aiohttp +import async_timeout + +from homeassistant.const import HTTP_ACCEPTED, MATCH_ALL, STATE_ON +from homeassistant.core import HomeAssistant, State, callback +from homeassistant.helpers.significant_change import create_checker +import homeassistant.util.dt as dt_util + +from .const import API_CHANGE, DOMAIN, Cause +from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id +from .messages import AlexaResponse + +_LOGGER = logging.getLogger(__name__) +DEFAULT_TIMEOUT = 10 + + +async def async_enable_proactive_mode(hass, smart_home_config): + """Enable the proactive mode. + + Proactive mode makes this component report state changes to Alexa. + """ + # Validate we can get access token. + await smart_home_config.async_get_access_token() + + @callback + def extra_significant_check( + hass: HomeAssistant, + old_state: str, + old_attrs: dict, + old_extra_arg: dict, + new_state: str, + new_attrs: dict, + new_extra_arg: dict, + ): + """Check if the serialized data has changed.""" + return old_extra_arg is not None and old_extra_arg != new_extra_arg + + checker = await create_checker(hass, DOMAIN, extra_significant_check) + + async def async_entity_state_listener( + changed_entity: str, + old_state: State | None, + new_state: State | None, + ): + if not hass.is_running: + return + + if not new_state: + return + + if new_state.domain not in ENTITY_ADAPTERS: + return + + if not smart_home_config.should_expose(changed_entity): + _LOGGER.debug("Not exposing %s because filtered by config", changed_entity) + return + + alexa_changed_entity: AlexaEntity = ENTITY_ADAPTERS[new_state.domain]( + hass, smart_home_config, new_state + ) + + # Determine how entity should be reported on + should_report = False + should_doorbell = False + + for interface in alexa_changed_entity.interfaces(): + if not should_report and interface.properties_proactively_reported(): + should_report = True + + if interface.name() == "Alexa.DoorbellEventSource": + should_doorbell = True + break + + if not should_report and not should_doorbell: + return + + if should_doorbell: + if new_state.state == STATE_ON: + await async_send_doorbell_event_message( + hass, smart_home_config, alexa_changed_entity + ) + return + + alexa_properties = list(alexa_changed_entity.serialize_properties()) + + if not checker.async_is_significant_change( + new_state, extra_arg=alexa_properties + ): + return + + await async_send_changereport_message( + hass, smart_home_config, alexa_changed_entity, alexa_properties + ) + + return hass.helpers.event.async_track_state_change( + MATCH_ALL, async_entity_state_listener + ) + + +async def async_send_changereport_message( + hass, config, alexa_entity, alexa_properties, *, invalidate_access_token=True +): + """Send a ChangeReport message for an Alexa entity. + + https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-with-changereport-events + """ + token = await config.async_get_access_token() + + headers = {"Authorization": f"Bearer {token}"} + + endpoint = alexa_entity.alexa_id() + + payload = { + API_CHANGE: { + "cause": {"type": Cause.APP_INTERACTION}, + "properties": alexa_properties, + } + } + + message = AlexaResponse(name="ChangeReport", namespace="Alexa", payload=payload) + message.set_endpoint_full(token, endpoint) + + message_serialized = message.serialize() + session = hass.helpers.aiohttp_client.async_get_clientsession() + + try: + with async_timeout.timeout(DEFAULT_TIMEOUT): + response = await session.post( + config.endpoint, + headers=headers, + json=message_serialized, + allow_redirects=True, + ) + + except (asyncio.TimeoutError, aiohttp.ClientError): + _LOGGER.error("Timeout sending report to Alexa") + return + + response_text = await response.text() + + _LOGGER.debug("Sent: %s", json.dumps(message_serialized)) + _LOGGER.debug("Received (%s): %s", response.status, response_text) + + if response.status == HTTP_ACCEPTED: + return + + response_json = json.loads(response_text) + + if ( + response_json["payload"]["code"] == "INVALID_ACCESS_TOKEN_EXCEPTION" + and not invalidate_access_token + ): + config.async_invalidate_access_token() + return await async_send_changereport_message( + hass, config, alexa_entity, alexa_properties, invalidate_access_token=False + ) + + _LOGGER.error( + "Error when sending ChangeReport to Alexa: %s: %s", + response_json["payload"]["code"], + response_json["payload"]["description"], + ) + + +async def async_send_add_or_update_message(hass, config, entity_ids): + """Send an AddOrUpdateReport message for entities. + + https://developer.amazon.com/docs/device-apis/alexa-discovery.html#add-or-update-report + """ + token = await config.async_get_access_token() + + headers = {"Authorization": f"Bearer {token}"} + + endpoints = [] + + for entity_id in entity_ids: + domain = entity_id.split(".", 1)[0] + + if domain not in ENTITY_ADAPTERS: + continue + + alexa_entity = ENTITY_ADAPTERS[domain](hass, config, hass.states.get(entity_id)) + endpoints.append(alexa_entity.serialize_discovery()) + + payload = {"endpoints": endpoints, "scope": {"type": "BearerToken", "token": token}} + + message = AlexaResponse( + name="AddOrUpdateReport", namespace="Alexa.Discovery", payload=payload + ) + + message_serialized = message.serialize() + session = hass.helpers.aiohttp_client.async_get_clientsession() + + return await session.post( + config.endpoint, headers=headers, json=message_serialized, allow_redirects=True + ) + + +async def async_send_delete_message(hass, config, entity_ids): + """Send an DeleteReport message for entities. + + https://developer.amazon.com/docs/device-apis/alexa-discovery.html#deletereport-event + """ + token = await config.async_get_access_token() + + headers = {"Authorization": f"Bearer {token}"} + + endpoints = [] + + for entity_id in entity_ids: + domain = entity_id.split(".", 1)[0] + + if domain not in ENTITY_ADAPTERS: + continue + + endpoints.append({"endpointId": generate_alexa_id(entity_id)}) + + payload = {"endpoints": endpoints, "scope": {"type": "BearerToken", "token": token}} + + message = AlexaResponse( + name="DeleteReport", namespace="Alexa.Discovery", payload=payload + ) + + message_serialized = message.serialize() + session = hass.helpers.aiohttp_client.async_get_clientsession() + + return await session.post( + config.endpoint, headers=headers, json=message_serialized, allow_redirects=True + ) + + +async def async_send_doorbell_event_message(hass, config, alexa_entity): + """Send a DoorbellPress event message for an Alexa entity. + + https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-doorbelleventsource.html + """ + token = await config.async_get_access_token() + + headers = {"Authorization": f"Bearer {token}"} + + endpoint = alexa_entity.alexa_id() + + message = AlexaResponse( + name="DoorbellPress", + namespace="Alexa.DoorbellEventSource", + payload={ + "cause": {"type": Cause.PHYSICAL_INTERACTION}, + "timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z", + }, + ) + + message.set_endpoint_full(token, endpoint) + + message_serialized = message.serialize() + session = hass.helpers.aiohttp_client.async_get_clientsession() + + try: + with async_timeout.timeout(DEFAULT_TIMEOUT): + response = await session.post( + config.endpoint, + headers=headers, + json=message_serialized, + allow_redirects=True, + ) + + except (asyncio.TimeoutError, aiohttp.ClientError): + _LOGGER.error("Timeout sending report to Alexa") + return + + response_text = await response.text() + + _LOGGER.debug("Sent: %s", json.dumps(message_serialized)) + _LOGGER.debug("Received (%s): %s", response.status, response_text) + + if response.status == HTTP_ACCEPTED: + return + + response_json = json.loads(response_text) + + _LOGGER.error( + "Error when sending DoorbellPress event to Alexa: %s: %s", + response_json["payload"]["code"], + response_json["payload"]["description"], + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/__init__.py new file mode 100644 index 00000000000..554a4aa47bc --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/__init__.py @@ -0,0 +1,311 @@ +"""Support for Almond.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta +import logging +import time + +from aiohttp import ClientError, ClientSession +import async_timeout +from pyalmond import AbstractAlmondWebAuth, AlmondLocalAuth, WebAlmondAPI +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.auth.const import GROUP_ID_ADMIN +from homeassistant.components import conversation +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_HOST, + CONF_TYPE, + EVENT_HOMEASSISTANT_START, +) +from homeassistant.core import Context, CoreState, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import ( + aiohttp_client, + config_entry_oauth2_flow, + config_validation as cv, + event, + intent, + network, + storage, +) + +from . import config_flow +from .const import DOMAIN, TYPE_LOCAL, TYPE_OAUTH2 + +STORAGE_VERSION = 1 +STORAGE_KEY = DOMAIN + +ALMOND_SETUP_DELAY = 30 + +DEFAULT_OAUTH2_HOST = "https://almond.stanford.edu" +DEFAULT_LOCAL_HOST = "http://localhost:3000" + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Any( + vol.Schema( + { + vol.Required(CONF_TYPE): TYPE_OAUTH2, + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_OAUTH2_HOST): cv.url, + } + ), + vol.Schema( + {vol.Required(CONF_TYPE): TYPE_LOCAL, vol.Required(CONF_HOST): cv.url} + ), + ) + }, + extra=vol.ALLOW_EXTRA, +) +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + """Set up the Almond component.""" + hass.data[DOMAIN] = {} + + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + + host = conf[CONF_HOST] + + if conf[CONF_TYPE] == TYPE_OAUTH2: + config_flow.AlmondFlowHandler.async_register_implementation( + hass, + config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, + DOMAIN, + conf[CONF_CLIENT_ID], + conf[CONF_CLIENT_SECRET], + f"{host}/me/api/oauth2/authorize", + f"{host}/me/api/oauth2/token", + ), + ) + return True + + if not hass.config_entries.async_entries(DOMAIN): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={"type": TYPE_LOCAL, "host": conf[CONF_HOST]}, + ) + ) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): + """Set up Almond config entry.""" + websession = aiohttp_client.async_get_clientsession(hass) + + if entry.data["type"] == TYPE_LOCAL: + auth = AlmondLocalAuth(entry.data["host"], websession) + else: + # OAuth2 + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + ) + oauth_session = config_entry_oauth2_flow.OAuth2Session( + hass, entry, implementation + ) + auth = AlmondOAuth(entry.data["host"], websession, oauth_session) + + api = WebAlmondAPI(auth) + agent = AlmondAgent(hass, api, entry) + + # Hass.io does its own configuration. + if not entry.data.get("is_hassio"): + # If we're not starting or local, set up Almond right away + if hass.state != CoreState.not_running or entry.data["type"] == TYPE_LOCAL: + await _configure_almond_for_ha(hass, entry, api) + + else: + # OAuth2 implementations can potentially rely on the HA Cloud url. + # This url is not be available until 30 seconds after boot. + + async def configure_almond(_now): + try: + await _configure_almond_for_ha(hass, entry, api) + except ConfigEntryNotReady: + _LOGGER.warning( + "Unable to configure Almond to connect to Home Assistant" + ) + + async def almond_hass_start(_event): + event.async_call_later(hass, ALMOND_SETUP_DELAY, configure_almond) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, almond_hass_start) + + conversation.async_set_agent(hass, agent) + return True + + +async def _configure_almond_for_ha( + hass: HomeAssistant, entry: config_entries.ConfigEntry, api: WebAlmondAPI +): + """Configure Almond to connect to HA.""" + try: + if entry.data["type"] == TYPE_OAUTH2: + # If we're connecting over OAuth2, we will only set up connection + # with Home Assistant if we're remotely accessible. + hass_url = network.get_url(hass, allow_internal=False, prefer_cloud=True) + else: + hass_url = network.get_url(hass) + except network.NoURLAvailableError: + # If no URL is available, we're not going to configure Almond to connect to HA. + return + + _LOGGER.debug("Configuring Almond to connect to Home Assistant at %s", hass_url) + store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) + data = await store.async_load() + + if data is None: + data = {} + + user = None + if "almond_user" in data: + user = await hass.auth.async_get_user(data["almond_user"]) + + if user is None: + user = await hass.auth.async_create_system_user("Almond", [GROUP_ID_ADMIN]) + data["almond_user"] = user.id + await store.async_save(data) + + refresh_token = await hass.auth.async_create_refresh_token( + user, + # Almond will be fine as long as we restart once every 5 years + access_token_expiration=timedelta(days=365 * 5), + ) + + # Create long lived access token + access_token = hass.auth.async_create_access_token(refresh_token) + + # Store token in Almond + try: + with async_timeout.timeout(30): + await api.async_create_device( + { + "kind": "io.home-assistant", + "hassUrl": hass_url, + "accessToken": access_token, + "refreshToken": "", + # 5 years from now in ms. + "accessTokenExpires": (time.time() + 60 * 60 * 24 * 365 * 5) * 1000, + } + ) + except (asyncio.TimeoutError, ClientError) as err: + if isinstance(err, asyncio.TimeoutError): + msg = "Request timeout" + else: + msg = err + _LOGGER.warning("Unable to configure Almond: %s", msg) + await hass.auth.async_remove_refresh_token(refresh_token) + raise ConfigEntryNotReady from err + + # Clear all other refresh tokens + for token in list(user.refresh_tokens.values()): + if token.id != refresh_token.id: + await hass.auth.async_remove_refresh_token(token) + + +async def async_unload_entry(hass, entry): + """Unload Almond.""" + conversation.async_set_agent(hass, None) + return True + + +class AlmondOAuth(AbstractAlmondWebAuth): + """Almond Authentication using OAuth2.""" + + def __init__( + self, + host: str, + websession: ClientSession, + oauth_session: config_entry_oauth2_flow.OAuth2Session, + ): + """Initialize Almond auth.""" + super().__init__(host, websession) + self._oauth_session = oauth_session + + async def async_get_access_token(self): + """Return a valid access token.""" + if not self._oauth_session.valid_token: + await self._oauth_session.async_ensure_token_valid() + + return self._oauth_session.token["access_token"] + + +class AlmondAgent(conversation.AbstractConversationAgent): + """Almond conversation agent.""" + + def __init__( + self, hass: HomeAssistant, api: WebAlmondAPI, entry: config_entries.ConfigEntry + ): + """Initialize the agent.""" + self.hass = hass + self.api = api + self.entry = entry + + @property + def attribution(self): + """Return the attribution.""" + return {"name": "Powered by Almond", "url": "https://almond.stanford.edu/"} + + async def async_get_onboarding(self): + """Get onboard url if not onboarded.""" + if self.entry.data.get("onboarded"): + return None + + host = self.entry.data["host"] + if self.entry.data.get("is_hassio"): + host = "/core_almond" + return { + "text": "Would you like to opt-in to share your anonymized commands with Stanford to improve Almond's responses?", + "url": f"{host}/conversation", + } + + async def async_set_onboarding(self, shown): + """Set onboarding status.""" + self.hass.config_entries.async_update_entry( + self.entry, data={**self.entry.data, "onboarded": shown} + ) + + return True + + async def async_process( + self, text: str, context: Context, conversation_id: str | None = None + ) -> intent.IntentResponse: + """Process a sentence.""" + response = await self.api.async_converse_text(text, conversation_id) + + first_choice = True + buffer = "" + for message in response["messages"]: + if message["type"] == "text": + buffer += f"\n{message['text']}" + elif message["type"] == "picture": + buffer += f"\n Picture: {message['url']}" + elif message["type"] == "rdl": + buffer += ( + f"\n Link: {message['rdl']['displayTitle']} " + f"{message['rdl']['webCallback']}" + ) + elif message["type"] == "choice": + if first_choice: + first_choice = False + else: + buffer += "," + buffer += f" {message['title']}" + + intent_result = intent.IntentResponse() + intent_result.async_set_speech(buffer.strip()) + return intent_result diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/config_flow.py new file mode 100644 index 00000000000..d6084569ff7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/config_flow.py @@ -0,0 +1,120 @@ +"""Config flow to connect with Home Assistant.""" +import asyncio +import logging + +from aiohttp import ClientError +import async_timeout +from pyalmond import AlmondLocalAuth, WebAlmondAPI +import voluptuous as vol +from yarl import URL + +from homeassistant import config_entries, core, data_entry_flow +from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow + +from .const import DOMAIN as ALMOND_DOMAIN, TYPE_LOCAL, TYPE_OAUTH2 + + +async def async_verify_local_connection(hass: core.HomeAssistant, host: str): + """Verify that a local connection works.""" + websession = aiohttp_client.async_get_clientsession(hass) + api = WebAlmondAPI(AlmondLocalAuth(host, websession)) + + try: + with async_timeout.timeout(10): + await api.async_list_apps() + + return True + except (asyncio.TimeoutError, ClientError): + return False + + +@config_entries.HANDLERS.register(ALMOND_DOMAIN) +class AlmondFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler): + """Implementation of the Almond OAuth2 config flow.""" + + DOMAIN = ALMOND_DOMAIN + + host = None + hassio_discovery = None + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + return {"scope": "profile user-read user-read-results user-exec-command"} + + async def async_step_user(self, user_input=None): + """Handle a flow start.""" + # Only allow 1 instance. + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + return await super().async_step_user(user_input) + + async def async_step_auth(self, user_input=None): + """Handle authorize step.""" + result = await super().async_step_auth(user_input) + + if result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP: + self.host = str(URL(result["url"]).with_path("me")) + + return result + + async def async_oauth_create_entry(self, data: dict) -> dict: + """Create an entry for the flow. + + Ok to override if you want to fetch extra info or even add another step. + """ + data["type"] = TYPE_OAUTH2 + data["host"] = self.host + return self.async_create_entry(title=self.flow_impl.name, data=data) + + async def async_step_import(self, user_input: dict = None) -> dict: + """Import data.""" + # Only allow 1 instance. + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if not await async_verify_local_connection(self.hass, user_input["host"]): + self.logger.warning( + "Aborting import of Almond because we're unable to connect" + ) + return self.async_abort(reason="cannot_connect") + + return self.async_create_entry( + title="Configuration.yaml", + data={"type": TYPE_LOCAL, "host": user_input["host"]}, + ) + + async def async_step_hassio(self, discovery_info): + """Receive a Hass.io discovery.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + self.hassio_discovery = discovery_info + + return await self.async_step_hassio_confirm() + + async def async_step_hassio_confirm(self, user_input=None): + """Confirm a Hass.io discovery.""" + data = self.hassio_discovery + + if user_input is not None: + return self.async_create_entry( + title=data["addon"], + data={ + "is_hassio": True, + "type": TYPE_LOCAL, + "host": f"http://{data['host']}:{data['port']}", + }, + ) + + return self.async_show_form( + step_id="hassio_confirm", + description_placeholders={"addon": data["addon"]}, + data_schema=vol.Schema({}), + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/const.py new file mode 100644 index 00000000000..34dca28e957 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/const.py @@ -0,0 +1,4 @@ +"""Constants for the Almond integration.""" +DOMAIN = "almond" +TYPE_OAUTH2 = "oauth2" +TYPE_LOCAL = "local" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/manifest.json new file mode 100644 index 00000000000..cd045f25715 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "almond", + "name": "Almond", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/almond", + "dependencies": ["http", "conversation"], + "codeowners": ["@gcampax", "@balloob"], + "requirements": ["pyalmond==0.0.2"], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/strings.json new file mode 100644 index 00000000000..548471a664c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + }, + "hassio_confirm": { + "title": "Almond via Home Assistant add-on", + "description": "Do you want to configure Home Assistant to connect to Almond provided by the add-on: {addon}?" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/bg.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/bg.json new file mode 100644 index 00000000000..bb0c874517b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/bg.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Almond \u0441\u044a\u0440\u0432\u044a\u0440\u0430.", + "missing_configuration": "\u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430 \u043a\u0430\u043a \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Almond." + }, + "step": { + "pick_implementation": { + "title": "\u0418\u0437\u0431\u043e\u0440 \u043d\u0430 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/ca.json new file mode 100644 index 00000000000..c4dcc2e38e2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", + "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "step": { + "hassio_confirm": { + "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb Almond proporcionat pel complement: {addon}?", + "title": "Almond via complement de Home Assistant" + }, + "pick_implementation": { + "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/cs.json new file mode 100644 index 00000000000..dc981403ad2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", + "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})", + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + }, + "step": { + "hassio_confirm": { + "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k Almond pomoc\u00ed Supervisor {addon}?", + "title": "Almond prost\u0159ednictv\u00edm dopl\u0148ku Supervisor" + }, + "pick_implementation": { + "title": "Vyberte metodu ov\u011b\u0159en\u00ed" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/da.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/da.json new file mode 100644 index 00000000000..0e7a804acc6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/da.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "cannot_connect": "Kan ikke oprette forbindelse til Almond-serveren.", + "missing_configuration": "Tjek venligst dokumentationen om, hvordan man indstiller Almond." + }, + "step": { + "hassio_confirm": { + "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til Almond leveret af Supervisor-tilf\u00f8jelsen: {addon}?", + "title": "Almond via Supervisor-tilf\u00f8jelse" + }, + "pick_implementation": { + "title": "V\u00e6lg godkendelsesmetode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/de.json new file mode 100644 index 00000000000..1f69b1c09e4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Verbindung fehlgeschlagen", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "hassio_confirm": { + "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass eine Verbindung mit Almond als Supervisor-Add-On hergestellt wird: {addon}?", + "title": "Almond \u00fcber das Supervisor Add-on" + }, + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/en.json new file mode 100644 index 00000000000..fb7d4127352 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Failed to connect", + "missing_configuration": "The component is not configured. Please follow the documentation.", + "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "hassio_confirm": { + "description": "Do you want to configure Home Assistant to connect to Almond provided by the add-on: {addon}?", + "title": "Almond via Home Assistant add-on" + }, + "pick_implementation": { + "title": "Pick Authentication Method" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/es-419.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/es-419.json new file mode 100644 index 00000000000..ce1d655d69e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "cannot_connect": "No se puede conectar con el servidor Almond.", + "missing_configuration": "Por favor, consulte la documentaci\u00f3n sobre c\u00f3mo configurar Almond." + }, + "step": { + "hassio_confirm": { + "description": "\u00bfDesea configurar Home Assistant para conectarse a Almond proporcionado por el complemento Supervisor: {addon}?", + "title": "Almond a trav\u00e9s del complemento Supervisor" + }, + "pick_implementation": { + "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/es.json new file mode 100644 index 00000000000..4dc5e4ee1c0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/es.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "No se puede conectar al servidor Almond.", + "missing_configuration": "Consulte la documentaci\u00f3n sobre c\u00f3mo configurar Almond.", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "step": { + "hassio_confirm": { + "description": "\u00bfDesea configurar Home Assistant para conectarse a Almond proporcionado por el complemento Supervisor: {addon} ?", + "title": "Almond a trav\u00e9s del complemento Supervisor" + }, + "pick_implementation": { + "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/et.json new file mode 100644 index 00000000000..5b15d9328cc --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u00dchendamine nurjus", + "missing_configuration": "Osis on seadistamata. Vaata dokumentatsiooni.", + "no_url_available": "URL pole saadaval. Rohkem teavet [spikrijaotis]({docs_url})", + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." + }, + "step": { + "hassio_confirm": { + "description": "Kas soovid seadistada Home Assistant-i \u00fchendust Almondiga mida pakub lisandmoodul: {addon} ?", + "title": "Almond Home Assistanti lisandmooduli abil" + }, + "pick_implementation": { + "title": "Vali tuvastusmeetod" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/fi.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/fi.json new file mode 100644 index 00000000000..33427bf8451 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/fi.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "Yhteyden muodostaminen Almond-palvelimeen ei onnistu." + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/fr.json new file mode 100644 index 00000000000..0e6a8e0be3f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Impossible de se connecter au serveur Almond", + "missing_configuration": "Veuillez consulter la documentation pour savoir comment configurer Almond.", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "step": { + "hassio_confirm": { + "description": "Voulez-vous configurer Home Assistant pour qu'il se connecte \u00e0 Almond fourni par le module compl\u00e9mentaire Hass.io: {addon} ?", + "title": "Amande via le module compl\u00e9mentaire Hass.io" + }, + "pick_implementation": { + "title": "S\u00e9lectionner une m\u00e9thode d'authentification" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/hu.json new file mode 100644 index 00000000000..568cd7270de --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "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.", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "hassio_confirm": { + "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Home Assistant alkalmaz\u00e1st az Almondhoz val\u00f3 csatlakoz\u00e1shoz, amelyet a Supervisor kieg\u00e9sz\u00edt\u0151 biztos\u00edt: {addon} ?", + "title": "Almond a Supervisor kieg\u00e9sz\u00edt\u0151n kereszt\u00fcl" + }, + "pick_implementation": { + "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/id.json new file mode 100644 index 00000000000..21a627132c4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Gagal terhubung", + "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", + "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "hassio_confirm": { + "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke Almond yang disediakan oleh add-on Supervisor {addon}?", + "title": "Almond melalui add-on Home Assistant" + }, + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/it.json new file mode 100644 index 00000000000..58eadad0d80 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Impossibile connettersi", + "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", + "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "step": { + "hassio_confirm": { + "description": "Vuoi configurare Home Assistant per connettersi a Almond fornito dal componente aggiuntivo: {addon}?", + "title": "Almond tramite il componente aggiuntivo di Home Assistant" + }, + "pick_implementation": { + "title": "Scegli il metodo di autenticazione" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/ko.json new file mode 100644 index 00000000000..d18f5c914cc --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/ko.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", + "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "step": { + "hassio_confirm": { + "description": "{addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 Almond\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Home Assistant \uc560\ub4dc\uc628\uc758 Almond" + }, + "pick_implementation": { + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/lb.json new file mode 100644 index 00000000000..0e29d69bbed --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/lb.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Feeler beim verbannen", + "missing_configuration": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun.", + "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})", + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." + }, + "step": { + "hassio_confirm": { + "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mam Almond ze verbannen dee vun der Supervisor Erweiderung {addon} bereet gestallt g\u00ebtt?", + "title": "Almond via Supervisor Erweiderung" + }, + "pick_implementation": { + "title": "Wiel Authentifikatiouns Method aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/nl.json new file mode 100644 index 00000000000..4c507cfab69 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Kan geen verbinding maken", + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." + }, + "step": { + "hassio_confirm": { + "description": "Wilt u Home Assistant configureren om verbinding te maken met Almond die wordt aangeboden door de add-on {addon} ?", + "title": "Almond via Home Assistant add-on" + }, + "pick_implementation": { + "title": "Kies een authenticatie methode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/no.json new file mode 100644 index 00000000000..098184ff7af --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Tilkobling mislyktes", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", + "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "step": { + "hassio_confirm": { + "description": "Vil du konfigurere Home Assistant for \u00e5 koble til Almond levert av tillegget: {addon} ?", + "title": "Almond via Home Assistant-tillegg" + }, + "pick_implementation": { + "title": "Velg godkjenningsmetode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/pl.json new file mode 100644 index 00000000000..88fd6cda01c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", + "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})", + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "step": { + "hassio_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 Home Assistant, aby \u0142\u0105czy\u0142 si\u0119 z Almond dostarczonym przez dodatek {addon}?", + "title": "Almond poprzez dodatek Home Assistant" + }, + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelniania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/pt-BR.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/pt-BR.json new file mode 100644 index 00000000000..94dfbefb86a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/pt-BR.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/pt.json new file mode 100644 index 00000000000..44f49239642 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/ru.json new file mode 100644 index 00000000000..b77e1cfca2c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "step": { + "hassio_confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \"{addon}\")?", + "title": "Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)" + }, + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/sl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/sl.json new file mode 100644 index 00000000000..cb20393201f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/sl.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "cannot_connect": "Ni mogo\u010de vzpostaviti povezave s stre\u017enikom Almond.", + "missing_configuration": "Prosimo, preverite dokumentacijo o tem, kako nastaviti Almond." + }, + "step": { + "hassio_confirm": { + "description": "Ali \u017eelite konfigurirati Home Assistant za povezavo z Almondom, ki ga ponuja dodatek Supervisor: {addon} ?", + "title": "Almond prek dodatka Supervisor" + }, + "pick_implementation": { + "title": "Izberite na\u010din preverjanja pristnosti" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/sv.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/sv.json new file mode 100644 index 00000000000..8b20512df9b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "cannot_connect": "Det g\u00e5r inte att ansluta till Almond-servern.", + "missing_configuration": "Kontrollera dokumentationen f\u00f6r hur du st\u00e4ller in Almond." + }, + "step": { + "hassio_confirm": { + "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till Almond som tillhandah\u00e5lls av Supervisor-till\u00e4gget: {addon} ?", + "title": "Almond via Supervisor-till\u00e4gget" + }, + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/tr.json new file mode 100644 index 00000000000..dc270099fcd --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/tr.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/uk.json new file mode 100644 index 00000000000..db96ef3d0a3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "step": { + "hassio_confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e Almond (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor \"{addon}\")?", + "title": "Almond (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor)" + }, + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/zh-Hant.json new file mode 100644 index 00000000000..9606a440aab --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/almond/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "step": { + "hassio_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 Almond\u3002\u9644\u52a0\u5143\u4ef6\u70ba\uff1a{addon} \uff1f", + "title": "\u4f7f\u7528 Home Assistant \u9644\u52a0\u5143\u4ef6 Almond" + }, + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alpha_vantage/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alpha_vantage/__init__.py new file mode 100644 index 00000000000..b8da9c19024 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alpha_vantage/__init__.py @@ -0,0 +1 @@ +"""The Alpha Vantage component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alpha_vantage/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/alpha_vantage/manifest.json new file mode 100644 index 00000000000..bfa41b3eeb1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alpha_vantage/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "alpha_vantage", + "name": "Alpha Vantage", + "documentation": "https://www.home-assistant.io/integrations/alpha_vantage", + "requirements": ["alpha_vantage==2.3.1"], + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/alpha_vantage/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/alpha_vantage/sensor.py new file mode 100644 index 00000000000..0788772a45b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/alpha_vantage/sensor.py @@ -0,0 +1,218 @@ +"""Stock market information from Alpha Vantage.""" +from datetime import timedelta +import logging + +from alpha_vantage.foreignexchange import ForeignExchange +from alpha_vantage.timeseries import TimeSeries +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_CURRENCY, CONF_NAME +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +ATTR_CLOSE = "close" +ATTR_HIGH = "high" +ATTR_LOW = "low" + +ATTRIBUTION = "Stock market information provided by Alpha Vantage" + +CONF_FOREIGN_EXCHANGE = "foreign_exchange" +CONF_FROM = "from" +CONF_SYMBOL = "symbol" +CONF_SYMBOLS = "symbols" +CONF_TO = "to" + +ICONS = { + "BTC": "mdi:currency-btc", + "EUR": "mdi:currency-eur", + "GBP": "mdi:currency-gbp", + "INR": "mdi:currency-inr", + "RUB": "mdi:currency-rub", + "TRY": "mdi:currency-try", + "USD": "mdi:currency-usd", +} + +SCAN_INTERVAL = timedelta(minutes=5) + +SYMBOL_SCHEMA = vol.Schema( + { + vol.Required(CONF_SYMBOL): cv.string, + vol.Optional(CONF_CURRENCY): cv.string, + vol.Optional(CONF_NAME): cv.string, + } +) + +CURRENCY_SCHEMA = vol.Schema( + { + vol.Required(CONF_FROM): cv.string, + vol.Required(CONF_TO): cv.string, + vol.Optional(CONF_NAME): cv.string, + } +) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_FOREIGN_EXCHANGE): vol.All(cv.ensure_list, [CURRENCY_SCHEMA]), + vol.Optional(CONF_SYMBOLS): vol.All(cv.ensure_list, [SYMBOL_SCHEMA]), + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Alpha Vantage sensor.""" + api_key = config[CONF_API_KEY] + symbols = config.get(CONF_SYMBOLS, []) + conversions = config.get(CONF_FOREIGN_EXCHANGE, []) + + if not symbols and not conversions: + msg = "No symbols or currencies configured." + hass.components.persistent_notification.create(msg, "Sensor alpha_vantage") + _LOGGER.warning(msg) + return + + timeseries = TimeSeries(key=api_key) + + dev = [] + for symbol in symbols: + try: + _LOGGER.debug("Configuring timeseries for symbols: %s", symbol[CONF_SYMBOL]) + timeseries.get_intraday(symbol[CONF_SYMBOL]) + except ValueError: + _LOGGER.error("API Key is not valid or symbol '%s' not known", symbol) + dev.append(AlphaVantageSensor(timeseries, symbol)) + + forex = ForeignExchange(key=api_key) + for conversion in conversions: + from_cur = conversion.get(CONF_FROM) + to_cur = conversion.get(CONF_TO) + try: + _LOGGER.debug("Configuring forex %s - %s", from_cur, to_cur) + forex.get_currency_exchange_rate(from_currency=from_cur, to_currency=to_cur) + except ValueError as error: + _LOGGER.error( + "API Key is not valid or currencies '%s'/'%s' not known", + from_cur, + to_cur, + ) + _LOGGER.debug(str(error)) + dev.append(AlphaVantageForeignExchange(forex, conversion)) + + add_entities(dev, True) + _LOGGER.debug("Setup completed") + + +class AlphaVantageSensor(SensorEntity): + """Representation of a Alpha Vantage sensor.""" + + def __init__(self, timeseries, symbol): + """Initialize the sensor.""" + self._symbol = symbol[CONF_SYMBOL] + self._name = symbol.get(CONF_NAME, self._symbol) + self._timeseries = timeseries + self.values = None + self._unit_of_measurement = symbol.get(CONF_CURRENCY, self._symbol) + self._icon = ICONS.get(symbol.get(CONF_CURRENCY, "USD")) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def state(self): + """Return the state of the sensor.""" + return self.values["1. open"] + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + if self.values is not None: + return { + ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_CLOSE: self.values["4. close"], + ATTR_HIGH: self.values["2. high"], + ATTR_LOW: self.values["3. low"], + } + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return self._icon + + def update(self): + """Get the latest data and updates the states.""" + _LOGGER.debug("Requesting new data for symbol %s", self._symbol) + all_values, _ = self._timeseries.get_intraday(self._symbol) + self.values = next(iter(all_values.values())) + _LOGGER.debug("Received new values for symbol %s", self._symbol) + + +class AlphaVantageForeignExchange(SensorEntity): + """Sensor for foreign exchange rates.""" + + def __init__(self, foreign_exchange, config): + """Initialize the sensor.""" + self._foreign_exchange = foreign_exchange + self._from_currency = config[CONF_FROM] + self._to_currency = config[CONF_TO] + if CONF_NAME in config: + self._name = config.get(CONF_NAME) + else: + self._name = f"{self._to_currency}/{self._from_currency}" + self._unit_of_measurement = self._to_currency + self._icon = ICONS.get(self._from_currency, "USD") + self.values = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def state(self): + """Return the state of the sensor.""" + return round(float(self.values["5. Exchange Rate"]), 4) + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return self._icon + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + if self.values is not None: + return { + ATTR_ATTRIBUTION: ATTRIBUTION, + CONF_FROM: self._from_currency, + CONF_TO: self._to_currency, + } + + def update(self): + """Get the latest data and updates the states.""" + _LOGGER.debug( + "Requesting new data for forex %s - %s", + self._from_currency, + self._to_currency, + ) + self.values, _ = self._foreign_exchange.get_currency_exchange_rate( + from_currency=self._from_currency, to_currency=self._to_currency + ) + _LOGGER.debug( + "Received new data for forex %s - %s", + self._from_currency, + self._to_currency, + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/amazon_polly/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/amazon_polly/__init__.py new file mode 100644 index 00000000000..0fab4af43e6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/amazon_polly/__init__.py @@ -0,0 +1 @@ +"""Support for Amazon Polly integration.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/amazon_polly/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/amazon_polly/const.py new file mode 100644 index 00000000000..5ae8c73881d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/amazon_polly/const.py @@ -0,0 +1,131 @@ +"""Constants for the Amazon Polly text to speech service.""" +from __future__ import annotations + +from typing import Final + +CONF_REGION: Final = "region_name" +CONF_ACCESS_KEY_ID: Final = "aws_access_key_id" +CONF_SECRET_ACCESS_KEY: Final = "aws_secret_access_key" + +DEFAULT_REGION: Final = "us-east-1" +SUPPORTED_REGIONS: Final[list[str]] = [ + "us-east-1", + "us-east-2", + "us-west-1", + "us-west-2", + "ca-central-1", + "eu-west-1", + "eu-central-1", + "eu-west-2", + "eu-west-3", + "ap-southeast-1", + "ap-southeast-2", + "ap-northeast-2", + "ap-northeast-1", + "ap-south-1", + "sa-east-1", +] + +CONF_ENGINE: Final = "engine" +CONF_VOICE: Final = "voice" +CONF_OUTPUT_FORMAT: Final = "output_format" +CONF_SAMPLE_RATE: Final = "sample_rate" +CONF_TEXT_TYPE: Final = "text_type" + +SUPPORTED_VOICES: Final[list[str]] = [ + "Olivia", # Female, Australian, Neural + "Zhiyu", # Chinese + "Mads", + "Naja", # Danish + "Ruben", + "Lotte", # Dutch + "Russell", + "Nicole", # English Australian + "Brian", + "Amy", + "Emma", # English + "Aditi", + "Raveena", # English, Indian + "Joey", + "Justin", + "Matthew", + "Ivy", + "Joanna", + "Kendra", + "Kimberly", + "Salli", # English + "Geraint", # English Welsh + "Mathieu", + "Celine", + "Lea", # French + "Chantal", # French Canadian + "Hans", + "Marlene", + "Vicki", # German + "Aditi", # Hindi + "Karl", + "Dora", # Icelandic + "Giorgio", + "Carla", + "Bianca", # Italian + "Takumi", + "Mizuki", # Japanese + "Seoyeon", # Korean + "Liv", # Norwegian + "Jacek", + "Jan", + "Ewa", + "Maja", # Polish + "Ricardo", + "Vitoria", # Portuguese, Brazilian + "Cristiano", + "Ines", # Portuguese, European + "Carmen", # Romanian + "Maxim", + "Tatyana", # Russian + "Enrique", + "Conchita", + "Lucia", # Spanish European + "Mia", # Spanish Mexican + "Miguel", # Spanish US + "Penelope", # Spanish US + "Lupe", # Spanish US + "Astrid", # Swedish + "Filiz", # Turkish + "Gwyneth", # Welsh +] + +SUPPORTED_OUTPUT_FORMATS: Final[list[str]] = ["mp3", "ogg_vorbis", "pcm"] + +SUPPORTED_ENGINES: Final[list[str]] = ["neural", "standard"] + +SUPPORTED_SAMPLE_RATES: Final[list[str]] = ["8000", "16000", "22050", "24000"] + +SUPPORTED_SAMPLE_RATES_MAP: Final[dict[str, list[str]]] = { + "mp3": ["8000", "16000", "22050", "24000"], + "ogg_vorbis": ["8000", "16000", "22050"], + "pcm": ["8000", "16000"], +} + +SUPPORTED_TEXT_TYPES: Final[list[str]] = ["text", "ssml"] + +CONTENT_TYPE_EXTENSIONS: Final[dict[str, str]] = { + "audio/mpeg": "mp3", + "audio/ogg": "ogg", + "audio/pcm": "pcm", +} + +DEFAULT_ENGINE: Final = "standard" +DEFAULT_VOICE: Final = "Joanna" +DEFAULT_OUTPUT_FORMAT: Final = "mp3" +DEFAULT_TEXT_TYPE: Final = "text" + +DEFAULT_SAMPLE_RATES: Final[dict[str, str]] = { + "mp3": "22050", + "ogg_vorbis": "22050", + "pcm": "16000", +} + +AWS_CONF_CONNECT_TIMEOUT: Final = 10 +AWS_CONF_READ_TIMEOUT: Final = 5 +AWS_CONF_MAX_POOL_CONNECTIONS: Final = 1 diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/amazon_polly/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/amazon_polly/manifest.json new file mode 100644 index 00000000000..779e320b0ab --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/amazon_polly/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "amazon_polly", + "name": "Amazon Polly", + "documentation": "https://www.home-assistant.io/integrations/amazon_polly", + "requirements": ["boto3==1.16.52"], + "codeowners": [], + "iot_class": "cloud_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/amazon_polly/tts.py b/homeassistant-2021.6.0.dev0/homeassistant/components/amazon_polly/tts.py new file mode 100644 index 00000000000..7e21b9ac603 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/amazon_polly/tts.py @@ -0,0 +1,196 @@ +"""Support for the Amazon Polly text to speech service.""" +from __future__ import annotations + +import logging +from typing import Final + +import boto3 +import botocore +import voluptuous as vol + +from homeassistant.components.tts import ( + PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, + Provider, + TtsAudioType, +) +from homeassistant.const import ATTR_CREDENTIALS, CONF_PROFILE_NAME +from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from .const import ( + AWS_CONF_CONNECT_TIMEOUT, + AWS_CONF_MAX_POOL_CONNECTIONS, + AWS_CONF_READ_TIMEOUT, + CONF_ACCESS_KEY_ID, + CONF_ENGINE, + CONF_OUTPUT_FORMAT, + CONF_REGION, + CONF_SAMPLE_RATE, + CONF_SECRET_ACCESS_KEY, + CONF_TEXT_TYPE, + CONF_VOICE, + CONTENT_TYPE_EXTENSIONS, + DEFAULT_ENGINE, + DEFAULT_OUTPUT_FORMAT, + DEFAULT_REGION, + DEFAULT_SAMPLE_RATES, + DEFAULT_TEXT_TYPE, + DEFAULT_VOICE, + SUPPORTED_ENGINES, + SUPPORTED_OUTPUT_FORMATS, + SUPPORTED_REGIONS, + SUPPORTED_SAMPLE_RATES, + SUPPORTED_SAMPLE_RATES_MAP, + SUPPORTED_TEXT_TYPES, + SUPPORTED_VOICES, +) + +_LOGGER: Final = logging.getLogger(__name__) + +PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_REGION, default=DEFAULT_REGION): vol.In(SUPPORTED_REGIONS), + vol.Inclusive(CONF_ACCESS_KEY_ID, ATTR_CREDENTIALS): cv.string, + vol.Inclusive(CONF_SECRET_ACCESS_KEY, ATTR_CREDENTIALS): cv.string, + vol.Exclusive(CONF_PROFILE_NAME, ATTR_CREDENTIALS): cv.string, + vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): vol.In(SUPPORTED_VOICES), + vol.Optional(CONF_ENGINE, default=DEFAULT_ENGINE): vol.In(SUPPORTED_ENGINES), + vol.Optional(CONF_OUTPUT_FORMAT, default=DEFAULT_OUTPUT_FORMAT): vol.In( + SUPPORTED_OUTPUT_FORMATS + ), + vol.Optional(CONF_SAMPLE_RATE): vol.All( + cv.string, vol.In(SUPPORTED_SAMPLE_RATES) + ), + vol.Optional(CONF_TEXT_TYPE, default=DEFAULT_TEXT_TYPE): vol.In( + SUPPORTED_TEXT_TYPES + ), + } +) + + +def get_engine( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> Provider | None: + """Set up Amazon Polly speech component.""" + output_format = config[CONF_OUTPUT_FORMAT] + sample_rate = config.get(CONF_SAMPLE_RATE, DEFAULT_SAMPLE_RATES[output_format]) + if sample_rate not in SUPPORTED_SAMPLE_RATES_MAP[output_format]: + _LOGGER.error( + "%s is not a valid sample rate for %s", sample_rate, output_format + ) + return None + + config[CONF_SAMPLE_RATE] = sample_rate + + profile: str | None = config.get(CONF_PROFILE_NAME) + + if profile is not None: + boto3.setup_default_session(profile_name=profile) + + aws_config = { + CONF_REGION: config[CONF_REGION], + CONF_ACCESS_KEY_ID: config.get(CONF_ACCESS_KEY_ID), + CONF_SECRET_ACCESS_KEY: config.get(CONF_SECRET_ACCESS_KEY), + "config": botocore.config.Config( + connect_timeout=AWS_CONF_CONNECT_TIMEOUT, + read_timeout=AWS_CONF_READ_TIMEOUT, + max_pool_connections=AWS_CONF_MAX_POOL_CONNECTIONS, + ), + } + + del config[CONF_REGION] + del config[CONF_ACCESS_KEY_ID] + del config[CONF_SECRET_ACCESS_KEY] + + polly_client = boto3.client("polly", **aws_config) + + supported_languages: list[str] = [] + + all_voices: dict[str, dict[str, str]] = {} + + all_voices_req = polly_client.describe_voices() + + for voice in all_voices_req.get("Voices", []): + voice_id: str | None = voice.get("Id") + if voice_id is None: + continue + all_voices[voice_id] = voice + language_code: str | None = voice.get("LanguageCode") + if language_code is not None and language_code not in supported_languages: + supported_languages.append(language_code) + + return AmazonPollyProvider(polly_client, config, supported_languages, all_voices) + + +class AmazonPollyProvider(Provider): + """Amazon Polly speech api provider.""" + + def __init__( + self, + polly_client: boto3.client, + config: ConfigType, + supported_languages: list[str], + all_voices: dict[str, dict[str, str]], + ) -> None: + """Initialize Amazon Polly provider for TTS.""" + self.client = polly_client + self.config = config + self.supported_langs = supported_languages + self.all_voices = all_voices + self.default_voice: str = self.config[CONF_VOICE] + self.name = "Amazon Polly" + + @property + def supported_languages(self) -> list[str]: + """Return a list of supported languages.""" + return self.supported_langs + + @property + def default_language(self) -> str | None: + """Return the default language.""" + return self.all_voices.get(self.default_voice, {}).get("LanguageCode") + + @property + def default_options(self) -> dict[str, str]: + """Return dict include default options.""" + return {CONF_VOICE: self.default_voice} + + @property + def supported_options(self) -> list[str]: + """Return a list of supported options.""" + return [CONF_VOICE] + + def get_tts_audio( + self, + message: str, + language: str | None = None, + options: dict[str, str] | None = None, + ) -> TtsAudioType: + """Request TTS file from Polly.""" + if options is None or language is None: + _LOGGER.debug("language and/or options were missing") + return None, None + voice_id = options.get(CONF_VOICE, self.default_voice) + voice_in_dict = self.all_voices[voice_id] + if language != voice_in_dict.get("LanguageCode"): + _LOGGER.error("%s does not support the %s language", voice_id, language) + return None, None + + _LOGGER.debug("Requesting TTS file for text: %s", message) + resp = self.client.synthesize_speech( + Engine=self.config[CONF_ENGINE], + OutputFormat=self.config[CONF_OUTPUT_FORMAT], + SampleRate=self.config[CONF_SAMPLE_RATE], + Text=message, + TextType=self.config[CONF_TEXT_TYPE], + VoiceId=voice_id, + ) + + _LOGGER.debug("Reply received for TTS: %s", message) + return ( + CONTENT_TYPE_EXTENSIONS[resp.get("ContentType")], + resp.get("AudioStream").read(), + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/__init__.py new file mode 100644 index 00000000000..e9247b9fd73 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/__init__.py @@ -0,0 +1,43 @@ +"""Support for Ambiclimate devices.""" +import voluptuous as vol + +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.helpers import config_validation as cv + +from . import config_flow +from .const import DOMAIN + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up Ambiclimate components.""" + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + + config_flow.register_flow_implementation( + hass, conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET] + ) + + return True + + +async def async_setup_entry(hass, entry): + """Set up Ambiclimate from a config entry.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "climate") + ) + + return True diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/climate.py b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/climate.py new file mode 100644 index 00000000000..93b38974464 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/climate.py @@ -0,0 +1,244 @@ +"""Support for Ambiclimate ac.""" +import asyncio +import logging + +import ambiclimate +import voluptuous as vol + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ( + ATTR_NAME, + ATTR_TEMPERATURE, + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + TEMP_CELSIUS, +) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import ( + ATTR_VALUE, + DOMAIN, + SERVICE_COMFORT_FEEDBACK, + SERVICE_COMFORT_MODE, + SERVICE_TEMPERATURE_MODE, + STORAGE_KEY, + STORAGE_VERSION, +) + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE + +SEND_COMFORT_FEEDBACK_SCHEMA = vol.Schema( + {vol.Required(ATTR_NAME): cv.string, vol.Required(ATTR_VALUE): cv.string} +) + +SET_COMFORT_MODE_SCHEMA = vol.Schema({vol.Required(ATTR_NAME): cv.string}) + +SET_TEMPERATURE_MODE_SCHEMA = vol.Schema( + {vol.Required(ATTR_NAME): cv.string, vol.Required(ATTR_VALUE): cv.string} +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Ambicliamte device.""" + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the Ambicliamte device from config entry.""" + config = entry.data + websession = async_get_clientsession(hass) + store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + token_info = await store.async_load() + + oauth = ambiclimate.AmbiclimateOAuth( + config[CONF_CLIENT_ID], + config[CONF_CLIENT_SECRET], + config["callback_url"], + websession, + ) + + try: + token_info = await oauth.refresh_access_token(token_info) + except ambiclimate.AmbiclimateOauthError: + token_info = None + + if not token_info: + _LOGGER.error("Failed to refresh access token") + return + + await store.async_save(token_info) + + data_connection = ambiclimate.AmbiclimateConnection( + oauth, token_info=token_info, websession=websession + ) + + if not await data_connection.find_devices(): + _LOGGER.error("No devices found") + return + + tasks = [] + for heater in data_connection.get_devices(): + tasks.append(heater.update_device_info()) + await asyncio.wait(tasks) + + devs = [] + for heater in data_connection.get_devices(): + devs.append(AmbiclimateEntity(heater, store)) + + async_add_entities(devs, True) + + async def send_comfort_feedback(service): + """Send comfort feedback.""" + device_name = service.data[ATTR_NAME] + device = data_connection.find_device_by_room_name(device_name) + if device: + await device.set_comfort_feedback(service.data[ATTR_VALUE]) + + hass.services.async_register( + DOMAIN, + SERVICE_COMFORT_FEEDBACK, + send_comfort_feedback, + schema=SEND_COMFORT_FEEDBACK_SCHEMA, + ) + + async def set_comfort_mode(service): + """Set comfort mode.""" + device_name = service.data[ATTR_NAME] + device = data_connection.find_device_by_room_name(device_name) + if device: + await device.set_comfort_mode() + + hass.services.async_register( + DOMAIN, SERVICE_COMFORT_MODE, set_comfort_mode, schema=SET_COMFORT_MODE_SCHEMA + ) + + async def set_temperature_mode(service): + """Set temperature mode.""" + device_name = service.data[ATTR_NAME] + device = data_connection.find_device_by_room_name(device_name) + if device: + await device.set_temperature_mode(service.data[ATTR_VALUE]) + + hass.services.async_register( + DOMAIN, + SERVICE_TEMPERATURE_MODE, + set_temperature_mode, + schema=SET_TEMPERATURE_MODE_SCHEMA, + ) + + +class AmbiclimateEntity(ClimateEntity): + """Representation of a Ambiclimate Thermostat device.""" + + def __init__(self, heater, store): + """Initialize the thermostat.""" + self._heater = heater + self._store = store + self._data = {} + + @property + def unique_id(self): + """Return a unique ID.""" + return self._heater.device_id + + @property + def name(self): + """Return the name of the entity.""" + return self._heater.name + + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Ambiclimate", + } + + @property + def temperature_unit(self): + """Return the unit of measurement which this thermostat uses.""" + return TEMP_CELSIUS + + @property + def target_temperature(self): + """Return the target temperature.""" + return self._data.get("target_temperature") + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return 1 + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._data.get("temperature") + + @property + def current_humidity(self): + """Return the current humidity.""" + return self._data.get("humidity") + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self._heater.get_min_temp() + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self._heater.get_max_temp() + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + @property + def hvac_modes(self): + """Return the list of available hvac operation modes.""" + return [HVAC_MODE_HEAT, HVAC_MODE_OFF] + + @property + def hvac_mode(self): + """Return current operation.""" + if self._data.get("power", "").lower() == "on": + return HVAC_MODE_HEAT + + return HVAC_MODE_OFF + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + await self._heater.set_target_temperature(temperature) + + async def async_set_hvac_mode(self, hvac_mode): + """Set new target hvac mode.""" + if hvac_mode == HVAC_MODE_HEAT: + await self._heater.turn_on() + return + if hvac_mode == HVAC_MODE_OFF: + await self._heater.turn_off() + + async def async_update(self): + """Retrieve latest state.""" + try: + token_info = await self._heater.control.refresh_access_token() + except ambiclimate.AmbiclimateOauthError: + _LOGGER.error("Failed to refresh access token") + return + + if token_info: + await self._store.async_save(token_info) + + self._data = await self._heater.update_device() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/config_flow.py new file mode 100644 index 00000000000..7ef0c5439aa --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/config_flow.py @@ -0,0 +1,153 @@ +"""Config flow for Ambiclimate.""" +import logging + +import ambiclimate + +from homeassistant import config_entries +from homeassistant.components.http import HomeAssistantView +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.network import get_url + +from .const import ( + AUTH_CALLBACK_NAME, + AUTH_CALLBACK_PATH, + DOMAIN, + STORAGE_KEY, + STORAGE_VERSION, +) + +DATA_AMBICLIMATE_IMPL = "ambiclimate_flow_implementation" + +_LOGGER = logging.getLogger(__name__) + + +@callback +def register_flow_implementation(hass, client_id, client_secret): + """Register a ambiclimate implementation. + + client_id: Client id. + client_secret: Client secret. + """ + hass.data.setdefault(DATA_AMBICLIMATE_IMPL, {}) + + hass.data[DATA_AMBICLIMATE_IMPL] = { + CONF_CLIENT_ID: client_id, + CONF_CLIENT_SECRET: client_secret, + } + + +class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + + def __init__(self): + """Initialize flow.""" + self._registered_view = False + self._oauth = None + + async def async_step_user(self, user_input=None): + """Handle external yaml configuration.""" + self._async_abort_entries_match() + + config = self.hass.data.get(DATA_AMBICLIMATE_IMPL, {}) + + if not config: + _LOGGER.debug("No config") + return self.async_abort(reason="missing_configuration") + + return await self.async_step_auth() + + async def async_step_auth(self, user_input=None): + """Handle a flow start.""" + self._async_abort_entries_match() + + errors = {} + + if user_input is not None: + errors["base"] = "follow_link" + + if not self._registered_view: + self._generate_view() + + return self.async_show_form( + step_id="auth", + description_placeholders={ + "authorization_url": await self._get_authorize_url(), + "cb_url": self._cb_url(), + }, + errors=errors, + ) + + async def async_step_code(self, code=None): + """Received code for authentication.""" + self._async_abort_entries_match() + + token_info = await self._get_token_info(code) + + if token_info is None: + return self.async_abort(reason="access_token") + + config = self.hass.data[DATA_AMBICLIMATE_IMPL].copy() + config["callback_url"] = self._cb_url() + + return self.async_create_entry(title="Ambiclimate", data=config) + + async def _get_token_info(self, code): + oauth = self._generate_oauth() + try: + token_info = await oauth.get_access_token(code) + except ambiclimate.AmbiclimateOauthError: + _LOGGER.error("Failed to get access token", exc_info=True) + return None + + store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + await store.async_save(token_info) + + return token_info + + def _generate_view(self): + self.hass.http.register_view(AmbiclimateAuthCallbackView()) + self._registered_view = True + + def _generate_oauth(self): + config = self.hass.data[DATA_AMBICLIMATE_IMPL] + clientsession = async_get_clientsession(self.hass) + callback_url = self._cb_url() + + return ambiclimate.AmbiclimateOAuth( + config.get(CONF_CLIENT_ID), + config.get(CONF_CLIENT_SECRET), + callback_url, + clientsession, + ) + + def _cb_url(self): + return f"{get_url(self.hass)}{AUTH_CALLBACK_PATH}" + + async def _get_authorize_url(self): + oauth = self._generate_oauth() + return oauth.get_authorize_url() + + +class AmbiclimateAuthCallbackView(HomeAssistantView): + """Ambiclimate Authorization Callback View.""" + + requires_auth = False + url = AUTH_CALLBACK_PATH + name = AUTH_CALLBACK_NAME + + async def get(self, request): + """Receive authorization token.""" + code = request.query.get("code") + if code is None: + return "No code" + hass = request.app["hass"] + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": "code"}, data=code + ) + ) + return "OK!" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/const.py new file mode 100644 index 00000000000..6393e97569a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/const.py @@ -0,0 +1,15 @@ +"""Constants used by the Ambiclimate component.""" + +DOMAIN = "ambiclimate" + +ATTR_VALUE = "value" + +SERVICE_COMFORT_FEEDBACK = "send_comfort_feedback" +SERVICE_COMFORT_MODE = "set_comfort_mode" +SERVICE_TEMPERATURE_MODE = "set_temperature_mode" + +STORAGE_KEY = "ambiclimate_auth" +STORAGE_VERSION = 1 + +AUTH_CALLBACK_NAME = "api:ambiclimate" +AUTH_CALLBACK_PATH = "/api/ambiclimate" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/manifest.json new file mode 100644 index 00000000000..9441cdb86bc --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "ambiclimate", + "name": "Ambiclimate", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ambiclimate", + "requirements": ["ambiclimate==0.2.1"], + "dependencies": ["http"], + "codeowners": ["@danielhiversen"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/services.yaml b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/services.yaml new file mode 100644 index 00000000000..f75857e4d2e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/services.yaml @@ -0,0 +1,54 @@ +# Describes the format for available services for ambiclimate + +set_comfort_mode: + name: Set comfort mode + description: > + Enable comfort mode on your AC. + fields: + Name: + description: > + String with device name. + required: true + example: Bedroom + selector: + text: + +send_comfort_feedback: + name: Send comfort feedback + description: > + Send feedback for comfort mode. + fields: + Name: + description: > + String with device name. + required: true + example: Bedroom + selector: + text: + Value: + description: > + Send any of the following comfort values: too_hot, too_warm, bit_warm, comfortable, bit_cold, too_cold, freezing + required: true + example: bit_warm + selector: + text: + +set_temperature_mode: + name: Set temperature mode + description: > + Enable temperature mode on your AC. + fields: + Name: + description: > + String with device name. + required: true + example: Bedroom + selector: + text: + Value: + description: > + Target value in celsius + required: true + example: 22 + selector: + text: diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/strings.json new file mode 100644 index 00000000000..c51c25a2f61 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "auth": { + "title": "Authenticate Ambiclimate", + "description": "Please follow this [link]({authorization_url}) and **Allow** access to your Ambiclimate account, then come back and press **Submit** below.\n(Make sure the specified callback URL is {cb_url})" + } + }, + "create_entry": { + "default": "[%key:common::config_flow::create_entry::authenticated%]" + }, + "error": { + "no_token": "Not authenticated with Ambiclimate", + "follow_link": "Please follow the link and authenticate before pressing Submit" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "access_token": "Unknown error generating an access token." + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/bg.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/bg.json new file mode 100644 index 00000000000..627dd472018 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "access_token": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f." + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435 \u0441 Ambiclimate." + }, + "error": { + "follow_link": "\u041c\u043e\u043b\u044f, \u043f\u043e\u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0432\u0440\u044a\u0437\u043a\u0430\u0442\u0430 \u0438 \u0441\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u0439\u0442\u0435, \u043f\u0440\u0435\u0434\u0438 \u0434\u0430 \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0418\u0437\u043f\u0440\u0430\u0449\u0430\u043d\u0435", + "no_token": "\u041b\u0438\u043f\u0441\u0432\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435 \u0441 Ambiclimate" + }, + "step": { + "auth": { + "description": "\u041c\u043e\u043b\u044f, \u043f\u043e\u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0442\u043e\u0437\u0438 [link]({authorization_url}) \u0438 **\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u0442\u0435** \u0434\u043e\u0441\u0442\u044a\u043f\u0430 \u0434\u043e \u043f\u0440\u043e\u0444\u0438\u043b\u0430 \u0441\u0438 \u0432 Ambiclimate, \u0441\u043b\u0435\u0434 \u0442\u043e\u0432\u0430 \u0441\u0435 \u0432\u044a\u0440\u043d\u0435\u0442\u0435 \u0438 \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 **\u0418\u0437\u043f\u0440\u0430\u0449\u0430\u043d\u0435** \u043f\u043e-\u0434\u043e\u043b\u0443. \n (\u0423\u0432\u0435\u0440\u0435\u0442\u0435 \u0441\u0435, \u0447\u0435 \u043f\u043e\u0441\u043e\u0447\u0435\u043d\u0438\u044f\u0442 url \u0437\u0430 \u043e\u0431\u0440\u0430\u0442\u043d\u0430 \u043f\u043e\u0432\u0438\u043a\u0432\u0430\u043d\u0435 \u0435 {cb_url})", + "title": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435 \u0441 Ambiclimate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/ca.json new file mode 100644 index 00000000000..8e54a222217 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "access_token": "S'ha produ\u00eft un error desconegut al generat un token d'acc\u00e9s.", + "already_configured": "El compte ja ha estat configurat", + "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3." + }, + "create_entry": { + "default": "Autenticaci\u00f3 exitosa" + }, + "error": { + "follow_link": "V\u00e9s a l'enlla\u00e7 i autentica't abans de pr\u00e9mer Envia", + "no_token": "No autenticat amb Ambi Climate" + }, + "step": { + "auth": { + "description": "V\u00e9s a l'[enlla\u00e7]({authorization_url}) i **Permet** l'acc\u00e9s al teu compte de Ambiclimate, despr\u00e9s torna i prem **Envia** a sota.\n(Assegura't que l'enlla\u00e7 de retorn \u00e9s el seg\u00fcent {cb_url})", + "title": "Autenticaci\u00f3 amb Ambi Climate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/cs.json new file mode 100644 index 00000000000..258b250ab4f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/cs.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "access_token": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed p\u0159\u00edstupov\u00e9ho tokenu.", + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace." + }, + "create_entry": { + "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno" + }, + "error": { + "follow_link": "N\u00e1sledujte odkaz a prove\u010fte ov\u011b\u0159en\u00ed p\u0159ed stisknut\u00edm tla\u010d\u00edtka Odeslat.", + "no_token": "Nen\u00ed ov\u011b\u0159en s Ambiclimate" + }, + "step": { + "auth": { + "description": "N\u00e1sledujte tento [odkaz]({authorization_url}) a **Povolit** p\u0159\u00edstup k va\u0161emu \u00fa\u010dtu Ambiclimate, pot\u00e9 se vra\u0165te a stiskn\u011bte **Odeslat** n\u00ed\u017ee. \n (Ujist\u011bte se, \u017ee zadan\u00e1 adresa URL zp\u011btn\u00e9ho vol\u00e1n\u00ed je {cb_url} )", + "title": "Ov\u011b\u0159it Ambiclimate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/da.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/da.json new file mode 100644 index 00000000000..c14016ca1d8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/da.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "access_token": "Ukendt fejl ved generering af et adgangstoken." + }, + "create_entry": { + "default": "Godkendt med Ambiclimate" + }, + "error": { + "follow_link": "F\u00f8lg linket og godkend f\u00f8r du trykker p\u00e5 send", + "no_token": "Ikke godkendt med Ambiclimate" + }, + "step": { + "auth": { + "description": "F\u00f8lg dette [link]({authorization_url}) og Tillad adgang til din Ambiclimate-konto, vend s\u00e5 tilbage og tryk p\u00e5 Indsend nedenfor.\n(Kontroll\u00e9r den angivne callback url er {cb_url})", + "title": "Godkend Ambiclimate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/de.json new file mode 100644 index 00000000000..d91fc15f37d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "access_token": "Unbekannter Fehler beim Generieren eines Zugriffstokens.", + "already_configured": "Konto wurde bereits konfiguriert", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen." + }, + "create_entry": { + "default": "Erfolgreiche Authentifizierung mit Ambiclimate" + }, + "error": { + "follow_link": "Bitte folge dem Link und authentifizieren dich, bevor du auf Senden klickst", + "no_token": "Nicht authentifiziert mit Ambiclimate" + }, + "step": { + "auth": { + "description": "Bitte folge diesem [link] ({authorization_url}) und **Erlaube** Zugriff auf dein Ambiclimate-Konto, komme dann zur\u00fcck und dr\u00fccke **Senden** darunter.\n (Pr\u00fcfe, dass die Callback-URL {cb_url} ist.)", + "title": "Ambiclimate authentifizieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/en.json new file mode 100644 index 00000000000..8621b0e247c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "access_token": "Unknown error generating an access token.", + "already_configured": "Account is already configured", + "missing_configuration": "The component is not configured. Please follow the documentation." + }, + "create_entry": { + "default": "Successfully authenticated" + }, + "error": { + "follow_link": "Please follow the link and authenticate before pressing Submit", + "no_token": "Not authenticated with Ambiclimate" + }, + "step": { + "auth": { + "description": "Please follow this [link]({authorization_url}) and **Allow** access to your Ambiclimate account, then come back and press **Submit** below.\n(Make sure the specified callback URL is {cb_url})", + "title": "Authenticate Ambiclimate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/es-419.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/es-419.json new file mode 100644 index 00000000000..8f1d915c0b1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/es-419.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "access_token": "Error desconocido al generar un token de acceso." + }, + "create_entry": { + "default": "Autenticaci\u00f3n exitosa con Ambiclimate" + }, + "error": { + "follow_link": "Por favor, siga el enlace y autent\u00edquese antes de presionar Enviar", + "no_token": "No autenticado con Ambiclimate" + }, + "step": { + "auth": { + "description": "Por favor, siga este [link]('authorization_url') y Permitir acceso a su cuenta de Ambiclimate, luego vuelva y presione Enviar a continuaci\u00f3n.\n(Aseg\u00farese de que la url de devoluci\u00f3n de llamada especificada es {cb_url})", + "title": "Autenticaci\u00f3n de Ambiclimate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/es.json new file mode 100644 index 00000000000..521234c972a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "access_token": "Error desconocido al generar un token de acceso.", + "already_configured": "La cuenta ya est\u00e1 configurada", + "missing_configuration": "El componente no est\u00e1 configurado. Siga la documentaci\u00f3n." + }, + "create_entry": { + "default": "Autenticado correctamente con Ambiclimate" + }, + "error": { + "follow_link": "Accede al enlace e identif\u00edcate antes de pulsar Enviar.", + "no_token": "No autenticado con Ambiclimate" + }, + "step": { + "auth": { + "description": "Accede al siguiente [enlace]({authorization_url}) y permite el acceso a tu cuenta de Ambiclimate, despu\u00e9s vuelve y pulsa en enviar a continuaci\u00f3n.\n(Aseg\u00farate que la url de devoluci\u00f3n de llamada es {cb_url})", + "title": "Autenticaci\u00f3n de Ambiclimate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/et.json new file mode 100644 index 00000000000..ff2264c3e0e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "access_token": "Tundmatu t\u00f5rge juurdep\u00e4\u00e4suloa loomisel.", + "already_configured": "Konto on juba seadistatud", + "missing_configuration": "Osis pole seadistatud. Palun vaata dokumentatsiooni." + }, + "create_entry": { + "default": "Ambiclimate autentimine \u00f5nnestus" + }, + "error": { + "follow_link": "Enne Esita nupu vajutamist j\u00e4rgi linki ja autendi", + "no_token": "Ambiclimate ei ole autenditud" + }, + "step": { + "auth": { + "description": "J\u00e4rgi seda linki [link] ( {authorization_url} ) ja ** Luba** juurdep\u00e4\u00e4s oma Ambiclimate'i kontole, siis tule tagasi ja vajuta allolevat nuppu ** Esita **.\n (Veendu, et m\u00e4\u00e4ratud tagasihelistamise URL on {cb_url} )", + "title": "Autendi Ambiclimate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/fr.json new file mode 100644 index 00000000000..37ef9549686 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "access_token": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'un jeton d'acc\u00e8s.", + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation." + }, + "create_entry": { + "default": "Authentifi\u00e9 avec succ\u00e8s avec Ambiclimate" + }, + "error": { + "follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur Soumettre.", + "no_token": "Non authentifi\u00e9 avec Ambiclimate" + }, + "step": { + "auth": { + "description": "Suivez ce [lien]({authorization_url}) et **Autorisez** l'acc\u00e8s \u00e0 votre compte Ambiclimate, puis revenez et appuyez sur **Envoyer** ci-dessous. \n (Assurez-vous que l'URL de rappel sp\u00e9cifi\u00e9 est {cb_url})", + "title": "Authentifier Ambiclimate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/hu.json new file mode 100644 index 00000000000..04035f04cca --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "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" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/id.json new file mode 100644 index 00000000000..66c30afcb09 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "access_token": "Terjadi kesalahan yang tidak diketahui saat membuat token akses.", + "already_configured": "Akun sudah dikonfigurasi", + "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi." + }, + "create_entry": { + "default": "Berhasil diautentikasi" + }, + "error": { + "follow_link": "Buka tautan dan autentikasi sebelum menekan Kirim", + "no_token": "Tidak diautentikasi dengan Ambiclimate" + }, + "step": { + "auth": { + "description": "Buka [tautan ini] ({authorization_url}) dan **Izinkan** akses ke akun Ambiclimate Anda, lalu kembali dan tekan **Kirim** di bawah ini.\n(Pastikan URL panggil balik yang ditentukan adalah {cb_url})", + "title": "Autentikasi Ambiclimate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/it.json new file mode 100644 index 00000000000..618d4cbe7ca --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "access_token": "Errore sconosciuto durante la generazione di un token di accesso.", + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione." + }, + "create_entry": { + "default": "Autenticazione riuscita" + }, + "error": { + "follow_link": "Si prega di seguire il link e di autenticarsi prima di premere Invia", + "no_token": "Non autenticato con Ambiclimate" + }, + "step": { + "auth": { + "description": "Segui questo [link]({authorization_url}) e **Consenti** l'accesso al tuo account Ambiclimate, quindi torna indietro e premi **Invia** qui sotto. \n(Assicurati che l'URL di richiamata specificato sia {cb_url})", + "title": "Autenticare Ambiclimate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/ka.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/ka.json new file mode 100644 index 00000000000..ed77d38ab45 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/ka.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0", + "missing_configuration": "\u10d4\u10e1 \u10d9\u10dd\u10db\u10de\u10dd\u10dc\u10d4\u10dc\u10e2\u10d8 \u10d0\u10e0 \u10d0\u10e0\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8. \u10d2\u10d7\u10ee\u10dd\u10d5\u10d7 \u10db\u10d8\u10e7\u10d5\u10d4\u10d7 \u10d3\u10dd\u10d9\u10e3\u10db\u10d4\u10dc\u10e2\u10d0\u10ea\u10d8\u10d0\u10e1" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/ko.json new file mode 100644 index 00000000000..e47c5041728 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070 \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." + }, + "create_entry": { + "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "follow_link": "\ud655\uc778\uc744 \ud074\ub9ad\ud558\uae30 \uc804\uc5d0 \ub9c1\ud06c\ub97c \ub530\ub77c \uc778\uc99d\uc744 \ubc1b\uc544\uc8fc\uc138\uc694", + "no_token": "Ambi Climate\ub85c \uc778\uc99d\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4" + }, + "step": { + "auth": { + "description": "[\ub9c1\ud06c]({authorization_url})(\uc744)\ub97c \ud074\ub9ad\ud558\uc5ec Ambiclimate \uacc4\uc815\uc5d0 \ub300\ud574 **\ud5c8\uc6a9**\ud55c \ub2e4\uc74c, \ub2e4\uc2dc \ub3cc\uc544\uc640\uc11c \ud558\ub2e8\uc758 **\ud655\uc778**\uc744 \ud074\ub9ad\ud574\uc8fc\uc138\uc694.\n(\ucf5c\ubc31 URL\uc774 {cb_url}(\uc73c)\ub85c \uc9c0\uc815\ub418\uc5c8\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694)", + "title": "Ambi Climate \uc778\uc99d\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/lb.json new file mode 100644 index 00000000000..ddb170db5b7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/lb.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "access_token": "Onbekannte Feeler beim gener\u00e9ieren vum Acc\u00e8s Jeton.", + "already_configured": "Kont ass scho konfigur\u00e9iert", + "missing_configuration": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun." + }, + "create_entry": { + "default": "Erfollegr\u00e4ich authentifiz\u00e9iert." + }, + "error": { + "follow_link": "Follegt w.e.g. dem Link an authentifiz\u00e9iert de Kont ier dir op ofsch\u00e9cken dr\u00e9ckt.", + "no_token": "Net mat Ambiclimate authentifiz\u00e9iert" + }, + "step": { + "auth": { + "description": "Follegt d\u00ebsem [Link]({authorization_url}) an ***erlaabt** den Acc\u00e8s zu \u00e4rem Ambiclimate Kont , a kommt dann zer\u00e9ck heihin an dr\u00e9ck op **ofsch\u00e9cken** hei \u00ebnnen.\n(Stellt s\u00e9cher dass den Type vun Callback {cb_url} ass.)", + "title": "Ambiclimate authentifiz\u00e9ieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/nl.json new file mode 100644 index 00000000000..6d3b3822224 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "access_token": "Onbekende fout bij het genereren van een toegangstoken.", + "already_configured": "Account is al geconfigureerd", + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen." + }, + "create_entry": { + "default": "Succesvol geauthenticeerd" + }, + "error": { + "follow_link": "Gelieve de link te volgen en te verifi\u00ebren voordat u op Verzenden drukt.", + "no_token": "Niet geverifieerd met Ambiclimate" + }, + "step": { + "auth": { + "description": "Volg deze [link]({authorization_url}) en klik op **Toestaan** om toegang te geven tot uw Ambiclimate-account, kom dan terug en druk hieronder op **Verzenden**. \n (Zorg ervoor dat de opgegeven callback-URL {cb_url})", + "title": "Authenticatie Ambiclimate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/no.json new file mode 100644 index 00000000000..6feaabadacc --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "access_token": "Ukjent feil ved oppretting av tilgangstoken.", + "already_configured": "Kontoen er allerede konfigurert", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen" + }, + "create_entry": { + "default": "Vellykket godkjenning" + }, + "error": { + "follow_link": "Vennligst f\u00f8lg lenken og godkjenn f\u00f8r du trykker p\u00e5 Send", + "no_token": "Ikke godkjent med Ambiclimate" + }, + "step": { + "auth": { + "description": "F\u00f8lg denne [linken]({authorization_url}) og **Tillat** tilgang til Ambiclimate-kontoen din, og kom deretter tilbake og trykk **Send** nedenfor.\n(Kontroller at den angitte url-adressen for tilbakeringing er {cb_url})", + "title": "Godkjenn Ambiclimate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/pl.json new file mode 100644 index 00000000000..2f90f3b4401 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "access_token": "Nieznany b\u0142\u0105d podczas generowania tokena dost\u0119pu", + "already_configured": "Konto jest ju\u017c skonfigurowane", + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono" + }, + "error": { + "follow_link": "Prosz\u0119 klikn\u0105\u0107 link i uwierzytelni\u0107 przed naci\u015bni\u0119ciem przycisku \"Zatwierd\u017a\"", + "no_token": "Nieuwierzytelniony z Ambiclimate" + }, + "step": { + "auth": { + "description": "Kliknij poni\u017cszy [link]({authorization_url}) i **Zezw\u00f3l** na dost\u0119p do konta Ambiclimate, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij **Zatwierd\u017a** poni\u017cej.\n(Upewnij si\u0119, \u017ce podany adres \"URL callback\" to {cb_url})", + "title": "Uwierzytelnienie Ambiclimate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/pt-BR.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/pt-BR.json new file mode 100644 index 00000000000..466096416ae --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "access_token": "Erro desconhecido ao gerar um token de acesso." + }, + "create_entry": { + "default": "Autenticado com sucesso no Ambiclimate" + }, + "error": { + "follow_link": "Por favor, siga o link e autentique-se antes de pressionar Enviar", + "no_token": "N\u00e3o autenticado com o Ambiclimate" + }, + "step": { + "auth": { + "description": "Por favor, siga este [link]({authorization_url}) e Permitir acesso \u00e0 sua conta Ambiclimate, em seguida, volte e pressione Enviar abaixo. \n (Verifique se a URL de retorno de chamada especificada \u00e9 {cb_url})", + "title": "Autenticar Ambiclimate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/pt.json new file mode 100644 index 00000000000..591d8c2feaa --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o." + }, + "create_entry": { + "default": "Autenticado com sucesso" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/ru.json new file mode 100644 index 00000000000..66ed1f5e43c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "access_token": "\u041f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430.", + "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.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439." + }, + "create_entry": { + "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "follow_link": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 \u0438 \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e, \u043f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043d\u0430\u0436\u0430\u0442\u044c \"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c\".", + "no_token": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0435 \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430." + }, + "step": { + "auth": { + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 **\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435** \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Ambi Climate, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**. \n(\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441 \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 {cb_url})", + "title": "Ambi Climate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/sl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/sl.json new file mode 100644 index 00000000000..8c923a7d213 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/sl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "access_token": "Neznana napaka pri ustvarjanju \u017eetona za dostop." + }, + "create_entry": { + "default": "Uspe\u0161no overjeno z funkcijo Ambiclimate" + }, + "error": { + "follow_link": "Preden pritisnete Po\u0161lji, sledite povezavi in preverite pristnost", + "no_token": "Ni overjeno z Ambiclimate" + }, + "step": { + "auth": { + "description": "Sledite temu povezavi ( {authorization_url} in Dovoli dostopu do svojega ra\u010duna Ambiclimate, nato se vrnite in pritisnite Po\u0161lji spodaj. \n (Poskrbite, da je dolo\u010den url za povratni klic {cb_url} )", + "title": "Overi Ambiclimate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/sv.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/sv.json new file mode 100644 index 00000000000..e6d06553d77 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "access_token": "Ok\u00e4nt fel vid generering av \u00e5tkomsttoken." + }, + "create_entry": { + "default": "Lyckad autentisering med Ambiclimate" + }, + "error": { + "follow_link": "V\u00e4nligen f\u00f6lj l\u00e4nken och autentisera dig innan du trycker p\u00e5 Skicka", + "no_token": "Inte autentiserad med Ambiclimate" + }, + "step": { + "auth": { + "description": "V\u00e4nligen f\u00f6lj denna [l\u00e4nk] ({authorization_url}) och till\u00e5ta till g\u00e5ng till ditt Ambiclimate konto, kom sedan tillbaka och tryck p\u00e5 Skicka nedan.\n(Kontrollera att den angivna callback url \u00e4r {cb_url})", + "title": "Autentisera Ambiclimate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/tr.json new file mode 100644 index 00000000000..bcaeba84558 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/uk.json new file mode 100644 index 00000000000..398665ab667 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "access_token": "\u041f\u0440\u0438 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u0456 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u0441\u0442\u0430\u043b\u0430\u0441\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430.", + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "error": { + "follow_link": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0437\u0430 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c \u0456 \u043f\u0440\u043e\u0439\u0434\u0456\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e, \u043f\u0435\u0440\u0448 \u043d\u0456\u0436 \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0438 \"\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438\".", + "no_token": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043d\u0435 \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430." + }, + "step": { + "auth": { + "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043f\u043e [\u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c]({authorization_url}) \u0456 ** \u0414\u043e\u0437\u0432\u043e\u043b\u044c\u0442\u0435 ** \u0434\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0432\u0430\u0448\u043e\u0433\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 Ambi Climate, \u043f\u043e\u0442\u0456\u043c \u043f\u043e\u0432\u0435\u0440\u043d\u0456\u0442\u044c\u0441\u044f \u0441\u044e\u0434\u0438 \u0456 \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **.\n(\u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0432\u043a\u0430\u0437\u0430\u043d\u0438\u0439 URL \u0437\u0432\u043e\u0440\u043e\u0442\u043d\u043e\u0433\u043e \u0432\u0438\u043a\u043b\u0438\u043a\u0443 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0430\u0454 {cb_url} )", + "title": "Ambi Climate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/zh-Hans.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/zh-Hans.json new file mode 100644 index 00000000000..df2d0ac6af9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/zh-Hans.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "\u8d26\u53f7\u5df2\u7ecf\u8bbe\u7f6e\u5b8c\u6210", + "missing_configuration": "\u7ec4\u4ef6\u5c1a\u672a\u914d\u7f6e\u3002\u8bf7\u53c2\u89c2\u6587\u4ef6\u8bf4\u660e\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/zh-Hant.json new file mode 100644 index 00000000000..e50accd7327 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambiclimate/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "access_token": "\u7522\u751f\u5b58\u53d6\u8a8d\u8b49\u78bc\u672a\u77e5\u932f\u8aa4\u3002", + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49" + }, + "error": { + "follow_link": "\u8acb\u65bc\u50b3\u9001\u524d\uff0c\u5148\u4f7f\u7528\u9023\u7d50\u4e26\u9032\u884c\u8a8d\u8b49\u3002", + "no_token": "Ambiclimate \u672a\u6388\u6b0a" + }, + "step": { + "auth": { + "description": "\u8acb\u4f7f\u7528\u6b64 [\u9023\u7d50]\uff08{authorization_url}\uff09\u4e26\u9ede\u9078**\u5141\u8a31**\u4ee5\u5b58\u53d6 Ambiclimate \u5e33\u865f\uff0c\u7136\u5f8c\u8fd4\u56de\u6b64\u9801\u9762\u4e26\u9ede\u9078\u4e0b\u65b9\u7684**\u50b3\u9001**\u3002\n\uff08\u78ba\u5b9a\u6307\u5b9a Callback URL \u70ba {cb_url}\uff09", + "title": "\u8a8d\u8b49 Ambiclimate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/__init__.py new file mode 100644 index 00000000000..9036a4d89a2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/__init__.py @@ -0,0 +1,573 @@ +"""Support for Ambient Weather Station Service.""" + +from aioambient import Client +from aioambient.errors import WebsocketError +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + DOMAIN as BINARY_SENSOR, +) +from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import ( + ATTR_LOCATION, + ATTR_NAME, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + CONF_API_KEY, + DEGREE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CO2, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, + EVENT_HOMEASSISTANT_STOP, + IRRADIATION_WATTS_PER_SQUARE_METER, + LIGHT_LUX, + PERCENTAGE, + PRESSURE_INHG, + SPEED_MILES_PER_HOUR, + TEMP_FAHRENHEIT, +) +from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_call_later + +from .const import ( + ATTR_LAST_DATA, + ATTR_MONITORED_CONDITIONS, + CONF_APP_KEY, + DATA_CLIENT, + DOMAIN, + LOGGER, +) + +PLATFORMS = [BINARY_SENSOR, SENSOR] + +DATA_CONFIG = "config" + +DEFAULT_SOCKET_MIN_RETRY = 15 + +TYPE_24HOURRAININ = "24hourrainin" +TYPE_BAROMABSIN = "baromabsin" +TYPE_BAROMRELIN = "baromrelin" +TYPE_BATT1 = "batt1" +TYPE_BATT10 = "batt10" +TYPE_BATT2 = "batt2" +TYPE_BATT3 = "batt3" +TYPE_BATT4 = "batt4" +TYPE_BATT5 = "batt5" +TYPE_BATT6 = "batt6" +TYPE_BATT7 = "batt7" +TYPE_BATT8 = "batt8" +TYPE_BATT9 = "batt9" +TYPE_BATT_CO2 = "batt_co2" +TYPE_BATTOUT = "battout" +TYPE_CO2 = "co2" +TYPE_DAILYRAININ = "dailyrainin" +TYPE_DEWPOINT = "dewPoint" +TYPE_EVENTRAININ = "eventrainin" +TYPE_FEELSLIKE = "feelsLike" +TYPE_HOURLYRAININ = "hourlyrainin" +TYPE_HUMIDITY = "humidity" +TYPE_HUMIDITY1 = "humidity1" +TYPE_HUMIDITY10 = "humidity10" +TYPE_HUMIDITY2 = "humidity2" +TYPE_HUMIDITY3 = "humidity3" +TYPE_HUMIDITY4 = "humidity4" +TYPE_HUMIDITY5 = "humidity5" +TYPE_HUMIDITY6 = "humidity6" +TYPE_HUMIDITY7 = "humidity7" +TYPE_HUMIDITY8 = "humidity8" +TYPE_HUMIDITY9 = "humidity9" +TYPE_HUMIDITYIN = "humidityin" +TYPE_LASTRAIN = "lastRain" +TYPE_MAXDAILYGUST = "maxdailygust" +TYPE_MONTHLYRAININ = "monthlyrainin" +TYPE_PM25 = "pm25" +TYPE_PM25_24H = "pm25_24h" +TYPE_PM25_BATT = "batt_25" +TYPE_PM25_IN = "pm25_in" +TYPE_PM25_IN_24H = "pm25_in_24h" +TYPE_PM25IN_BATT = "batt_25in" +TYPE_RELAY1 = "relay1" +TYPE_RELAY10 = "relay10" +TYPE_RELAY2 = "relay2" +TYPE_RELAY3 = "relay3" +TYPE_RELAY4 = "relay4" +TYPE_RELAY5 = "relay5" +TYPE_RELAY6 = "relay6" +TYPE_RELAY7 = "relay7" +TYPE_RELAY8 = "relay8" +TYPE_RELAY9 = "relay9" +TYPE_SOILHUM1 = "soilhum1" +TYPE_SOILHUM10 = "soilhum10" +TYPE_SOILHUM2 = "soilhum2" +TYPE_SOILHUM3 = "soilhum3" +TYPE_SOILHUM4 = "soilhum4" +TYPE_SOILHUM5 = "soilhum5" +TYPE_SOILHUM6 = "soilhum6" +TYPE_SOILHUM7 = "soilhum7" +TYPE_SOILHUM8 = "soilhum8" +TYPE_SOILHUM9 = "soilhum9" +TYPE_SOILTEMP1F = "soiltemp1f" +TYPE_SOILTEMP10F = "soiltemp10f" +TYPE_SOILTEMP2F = "soiltemp2f" +TYPE_SOILTEMP3F = "soiltemp3f" +TYPE_SOILTEMP4F = "soiltemp4f" +TYPE_SOILTEMP5F = "soiltemp5f" +TYPE_SOILTEMP6F = "soiltemp6f" +TYPE_SOILTEMP7F = "soiltemp7f" +TYPE_SOILTEMP8F = "soiltemp8f" +TYPE_SOILTEMP9F = "soiltemp9f" +TYPE_SOLARRADIATION = "solarradiation" +TYPE_SOLARRADIATION_LX = "solarradiation_lx" +TYPE_TEMP10F = "temp10f" +TYPE_TEMP1F = "temp1f" +TYPE_TEMP2F = "temp2f" +TYPE_TEMP3F = "temp3f" +TYPE_TEMP4F = "temp4f" +TYPE_TEMP5F = "temp5f" +TYPE_TEMP6F = "temp6f" +TYPE_TEMP7F = "temp7f" +TYPE_TEMP8F = "temp8f" +TYPE_TEMP9F = "temp9f" +TYPE_TEMPF = "tempf" +TYPE_TEMPINF = "tempinf" +TYPE_TOTALRAININ = "totalrainin" +TYPE_UV = "uv" +TYPE_WEEKLYRAININ = "weeklyrainin" +TYPE_WINDDIR = "winddir" +TYPE_WINDDIR_AVG10M = "winddir_avg10m" +TYPE_WINDDIR_AVG2M = "winddir_avg2m" +TYPE_WINDGUSTDIR = "windgustdir" +TYPE_WINDGUSTMPH = "windgustmph" +TYPE_WINDSPDMPH_AVG10M = "windspdmph_avg10m" +TYPE_WINDSPDMPH_AVG2M = "windspdmph_avg2m" +TYPE_WINDSPEEDMPH = "windspeedmph" +TYPE_YEARLYRAININ = "yearlyrainin" +SENSOR_TYPES = { + TYPE_24HOURRAININ: ("24 Hr Rain", "in", SENSOR, None), + TYPE_BAROMABSIN: ("Abs Pressure", PRESSURE_INHG, SENSOR, DEVICE_CLASS_PRESSURE), + TYPE_BAROMRELIN: ("Rel Pressure", PRESSURE_INHG, SENSOR, DEVICE_CLASS_PRESSURE), + TYPE_BATT10: ("Battery 10", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_BATT1: ("Battery 1", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_BATT2: ("Battery 2", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_BATT3: ("Battery 3", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_BATT4: ("Battery 4", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_BATT5: ("Battery 5", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_BATT6: ("Battery 6", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_BATT7: ("Battery 7", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_BATT8: ("Battery 8", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_BATT9: ("Battery 9", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_BATTOUT: ("Battery", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_BATT_CO2: ("CO2 Battery", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_CO2: ("co2", CONCENTRATION_PARTS_PER_MILLION, SENSOR, DEVICE_CLASS_CO2), + TYPE_DAILYRAININ: ("Daily Rain", "in", SENSOR, None), + TYPE_DEWPOINT: ("Dew Point", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_EVENTRAININ: ("Event Rain", "in", SENSOR, None), + TYPE_FEELSLIKE: ("Feels Like", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_HOURLYRAININ: ("Hourly Rain Rate", "in/hr", SENSOR, None), + TYPE_HUMIDITY10: ("Humidity 10", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_HUMIDITY1: ("Humidity 1", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_HUMIDITY2: ("Humidity 2", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_HUMIDITY3: ("Humidity 3", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_HUMIDITY4: ("Humidity 4", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_HUMIDITY5: ("Humidity 5", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_HUMIDITY6: ("Humidity 6", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_HUMIDITY7: ("Humidity 7", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_HUMIDITY8: ("Humidity 8", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_HUMIDITY9: ("Humidity 9", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_HUMIDITY: ("Humidity", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_HUMIDITYIN: ("Humidity In", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_LASTRAIN: ("Last Rain", None, SENSOR, DEVICE_CLASS_TIMESTAMP), + TYPE_MAXDAILYGUST: ("Max Gust", SPEED_MILES_PER_HOUR, SENSOR, None), + TYPE_MONTHLYRAININ: ("Monthly Rain", "in", SENSOR, None), + TYPE_PM25_24H: ( + "PM25 24h Avg", + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + SENSOR, + None, + ), + TYPE_PM25_BATT: ("PM25 Battery", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_PM25_IN: ( + "PM25 Indoor", + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + SENSOR, + None, + ), + TYPE_PM25_IN_24H: ( + "PM25 Indoor 24h Avg", + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + SENSOR, + None, + ), + TYPE_PM25: ("PM25", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, SENSOR, None), + TYPE_PM25IN_BATT: ( + "PM25 Indoor Battery", + None, + BINARY_SENSOR, + DEVICE_CLASS_BATTERY, + ), + TYPE_RELAY10: ("Relay 10", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY1: ("Relay 1", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY2: ("Relay 2", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY3: ("Relay 3", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY4: ("Relay 4", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY5: ("Relay 5", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY6: ("Relay 6", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY7: ("Relay 7", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY8: ("Relay 8", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY9: ("Relay 9", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_SOILHUM10: ("Soil Humidity 10", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_SOILHUM1: ("Soil Humidity 1", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_SOILHUM2: ("Soil Humidity 2", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_SOILHUM3: ("Soil Humidity 3", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_SOILHUM4: ("Soil Humidity 4", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_SOILHUM5: ("Soil Humidity 5", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_SOILHUM6: ("Soil Humidity 6", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_SOILHUM7: ("Soil Humidity 7", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_SOILHUM8: ("Soil Humidity 8", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_SOILHUM9: ("Soil Humidity 9", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_SOILTEMP10F: ( + "Soil Temp 10", + TEMP_FAHRENHEIT, + SENSOR, + DEVICE_CLASS_TEMPERATURE, + ), + TYPE_SOILTEMP1F: ("Soil Temp 1", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_SOILTEMP2F: ("Soil Temp 2", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_SOILTEMP3F: ("Soil Temp 3", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_SOILTEMP4F: ("Soil Temp 4", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_SOILTEMP5F: ("Soil Temp 5", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_SOILTEMP6F: ("Soil Temp 6", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_SOILTEMP7F: ("Soil Temp 7", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_SOILTEMP8F: ("Soil Temp 8", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_SOILTEMP9F: ("Soil Temp 9", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_SOLARRADIATION: ( + "Solar Rad", + IRRADIATION_WATTS_PER_SQUARE_METER, + SENSOR, + None, + ), + TYPE_SOLARRADIATION_LX: ( + "Solar Rad (lx)", + LIGHT_LUX, + SENSOR, + DEVICE_CLASS_ILLUMINANCE, + ), + TYPE_TEMP10F: ("Temp 10", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TEMP1F: ("Temp 1", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TEMP2F: ("Temp 2", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TEMP3F: ("Temp 3", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TEMP4F: ("Temp 4", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TEMP5F: ("Temp 5", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TEMP6F: ("Temp 6", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TEMP7F: ("Temp 7", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TEMP8F: ("Temp 8", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TEMP9F: ("Temp 9", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TEMPF: ("Temp", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TEMPINF: ("Inside Temp", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TOTALRAININ: ("Lifetime Rain", "in", SENSOR, None), + TYPE_UV: ("uv", "Index", SENSOR, None), + TYPE_WEEKLYRAININ: ("Weekly Rain", "in", SENSOR, None), + TYPE_WINDDIR: ("Wind Dir", DEGREE, SENSOR, None), + TYPE_WINDDIR_AVG10M: ("Wind Dir Avg 10m", DEGREE, SENSOR, None), + TYPE_WINDDIR_AVG2M: ("Wind Dir Avg 2m", SPEED_MILES_PER_HOUR, SENSOR, None), + TYPE_WINDGUSTDIR: ("Gust Dir", DEGREE, SENSOR, None), + TYPE_WINDGUSTMPH: ("Wind Gust", SPEED_MILES_PER_HOUR, SENSOR, None), + TYPE_WINDSPDMPH_AVG10M: ("Wind Avg 10m", SPEED_MILES_PER_HOUR, SENSOR, None), + TYPE_WINDSPDMPH_AVG2M: ("Wind Avg 2m", SPEED_MILES_PER_HOUR, SENSOR, None), + TYPE_WINDSPEEDMPH: ("Wind Speed", SPEED_MILES_PER_HOUR, SENSOR, None), + TYPE_YEARLYRAININ: ("Yearly Rain", "in", SENSOR, None), +} + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_APP_KEY): cv.string, + vol.Required(CONF_API_KEY): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the Ambient PWS integration.""" + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_CLIENT] = {} + + if DOMAIN not in config: + return True + conf = config[DOMAIN] + + # Store config for use during entry setup: + hass.data[DOMAIN][DATA_CONFIG] = conf + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_API_KEY: conf[CONF_API_KEY], CONF_APP_KEY: conf[CONF_APP_KEY]}, + ) + ) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up the Ambient PWS as config entry.""" + if not config_entry.unique_id: + hass.config_entries.async_update_entry( + config_entry, unique_id=config_entry.data[CONF_APP_KEY] + ) + session = aiohttp_client.async_get_clientsession(hass) + + try: + ambient = AmbientStation( + hass, + config_entry, + Client( + config_entry.data[CONF_API_KEY], + config_entry.data[CONF_APP_KEY], + session=session, + ), + ) + hass.loop.create_task(ambient.ws_connect()) + hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = ambient + except WebsocketError as err: + LOGGER.error("Config entry failed: %s", err) + raise ConfigEntryNotReady from err + + async def _async_disconnect_websocket(*_): + await ambient.client.websocket.disconnect() + + config_entry.async_on_unload( + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_disconnect_websocket + ) + ) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload an Ambient PWS config entry.""" + ambient = hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) + hass.async_create_task(ambient.ws_disconnect()) + + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) + + +async def async_migrate_entry(hass, config_entry): + """Migrate old entry.""" + version = config_entry.version + + LOGGER.debug("Migrating from version %s", version) + + # 1 -> 2: Unique ID format changed, so delete and re-import: + if version == 1: + dev_reg = await hass.helpers.device_registry.async_get_registry() + dev_reg.async_clear_config_entry(config_entry) + + en_reg = await hass.helpers.entity_registry.async_get_registry() + en_reg.async_clear_config_entry(config_entry) + + version = config_entry.version = 2 + hass.config_entries.async_update_entry(config_entry) + LOGGER.info("Migration to version %s successful", version) + + return True + + +class AmbientStation: + """Define a class to handle the Ambient websocket.""" + + def __init__(self, hass, config_entry, client): + """Initialize.""" + self._config_entry = config_entry + self._entry_setup_complete = False + self._hass = hass + self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY + self.client = client + self.stations = {} + + async def _attempt_connect(self): + """Attempt to connect to the socket (retrying later on fail).""" + + async def connect(timestamp=None): + """Connect.""" + await self.client.websocket.connect() + + try: + await connect() + except WebsocketError as err: + LOGGER.error("Error with the websocket connection: %s", err) + self._ws_reconnect_delay = min(2 * self._ws_reconnect_delay, 480) + async_call_later(self._hass, self._ws_reconnect_delay, connect) + + async def ws_connect(self): + """Register handlers and connect to the websocket.""" + + def on_connect(): + """Define a handler to fire when the websocket is connected.""" + LOGGER.info("Connected to websocket") + + def on_data(data): + """Define a handler to fire when the data is received.""" + mac_address = data["macAddress"] + if data != self.stations[mac_address][ATTR_LAST_DATA]: + LOGGER.debug("New data received: %s", data) + self.stations[mac_address][ATTR_LAST_DATA] = data + async_dispatcher_send( + self._hass, f"ambient_station_data_update_{mac_address}" + ) + + def on_disconnect(): + """Define a handler to fire when the websocket is disconnected.""" + LOGGER.info("Disconnected from websocket") + + def on_subscribed(data): + """Define a handler to fire when the subscription is set.""" + for station in data["devices"]: + if station["macAddress"] in self.stations: + continue + LOGGER.debug("New station subscription: %s", data) + + # Only create entities based on the data coming through the socket. + # If the user is monitoring brightness (in W/m^2), make sure we also + # add a calculated sensor for the same data measured in lx: + monitored_conditions = [ + k for k in station["lastData"] if k in SENSOR_TYPES + ] + if TYPE_SOLARRADIATION in monitored_conditions: + monitored_conditions.append(TYPE_SOLARRADIATION_LX) + self.stations[station["macAddress"]] = { + ATTR_LAST_DATA: station["lastData"], + ATTR_LOCATION: station.get("info", {}).get("location"), + ATTR_MONITORED_CONDITIONS: monitored_conditions, + ATTR_NAME: station.get("info", {}).get( + "name", station["macAddress"] + ), + } + # If the websocket disconnects and reconnects, the on_subscribed + # handler will get called again; in that case, we don't want to + # attempt forward setup of the config entry (because it will have + # already been done): + if not self._entry_setup_complete: + self._hass.config_entries.async_setup_platforms( + self._config_entry, PLATFORMS + ) + self._entry_setup_complete = True + self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY + + self.client.websocket.on_connect(on_connect) + self.client.websocket.on_data(on_data) + self.client.websocket.on_disconnect(on_disconnect) + self.client.websocket.on_subscribed(on_subscribed) + + await self._attempt_connect() + + async def ws_disconnect(self): + """Disconnect from the websocket.""" + await self.client.websocket.disconnect() + + +class AmbientWeatherEntity(Entity): + """Define a base Ambient PWS entity.""" + + def __init__( + self, ambient, mac_address, station_name, sensor_type, sensor_name, device_class + ): + """Initialize the sensor.""" + self._ambient = ambient + self._device_class = device_class + self._mac_address = mac_address + self._sensor_name = sensor_name + self._sensor_type = sensor_type + self._state = None + self._station_name = station_name + + @property + def available(self): + """Return True if entity is available.""" + # Since the solarradiation_lx sensor is created only if the + # user shows a solarradiation sensor, ensure that the + # solarradiation_lx sensor shows as available if the solarradiation + # sensor is available: + if self._sensor_type == TYPE_SOLARRADIATION_LX: + return ( + self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get( + TYPE_SOLARRADIATION + ) + is not None + ) + return ( + self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get( + self._sensor_type + ) + is not None + ) + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @property + def device_info(self): + """Return device registry information for this entity.""" + return { + "identifiers": {(DOMAIN, self._mac_address)}, + "name": self._station_name, + "manufacturer": "Ambient Weather", + } + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self._station_name}_{self._sensor_name}" + + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def unique_id(self): + """Return a unique, unchanging string that represents this sensor.""" + return f"{self._mac_address}_{self._sensor_type}" + + async def async_added_to_hass(self): + """Register callbacks.""" + + @callback + def update(): + """Update the state.""" + self.update_from_latest_data() + self.async_write_ha_state() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, f"ambient_station_data_update_{self._mac_address}", update + ) + ) + + self.update_from_latest_data() + + @callback + def update_from_latest_data(self): + """Update the entity from the latest data.""" + raise NotImplementedError diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/binary_sensor.py new file mode 100644 index 00000000000..c2e5ad8b4f4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/binary_sensor.py @@ -0,0 +1,84 @@ +"""Support for Ambient Weather Station binary sensors.""" +from homeassistant.components.binary_sensor import ( + DOMAIN as BINARY_SENSOR, + BinarySensorEntity, +) +from homeassistant.const import ATTR_NAME +from homeassistant.core import callback + +from . import ( + SENSOR_TYPES, + TYPE_BATT1, + TYPE_BATT2, + TYPE_BATT3, + TYPE_BATT4, + TYPE_BATT5, + TYPE_BATT6, + TYPE_BATT7, + TYPE_BATT8, + TYPE_BATT9, + TYPE_BATT10, + TYPE_BATT_CO2, + TYPE_BATTOUT, + TYPE_PM25_BATT, + TYPE_PM25IN_BATT, + AmbientWeatherEntity, +) +from .const import ATTR_LAST_DATA, ATTR_MONITORED_CONDITIONS, DATA_CLIENT, DOMAIN + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Ambient PWS binary sensors based on a config entry.""" + ambient = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + + binary_sensor_list = [] + for mac_address, station in ambient.stations.items(): + for condition in station[ATTR_MONITORED_CONDITIONS]: + name, _, kind, device_class = SENSOR_TYPES[condition] + if kind == BINARY_SENSOR: + binary_sensor_list.append( + AmbientWeatherBinarySensor( + ambient, + mac_address, + station[ATTR_NAME], + condition, + name, + device_class, + ) + ) + + async_add_entities(binary_sensor_list, True) + + +class AmbientWeatherBinarySensor(AmbientWeatherEntity, BinarySensorEntity): + """Define an Ambient binary sensor.""" + + @property + def is_on(self): + """Return the status of the sensor.""" + if self._sensor_type in ( + TYPE_BATT1, + TYPE_BATT10, + TYPE_BATT2, + TYPE_BATT3, + TYPE_BATT4, + TYPE_BATT5, + TYPE_BATT6, + TYPE_BATT7, + TYPE_BATT8, + TYPE_BATT9, + TYPE_BATT_CO2, + TYPE_BATTOUT, + TYPE_PM25_BATT, + TYPE_PM25IN_BATT, + ): + return self._state == 0 + + return self._state == 1 + + @callback + def update_from_latest_data(self): + """Fetch new state data for the entity.""" + self._state = self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get( + self._sensor_type + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/config_flow.py new file mode 100644 index 00000000000..429388dcaba --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/config_flow.py @@ -0,0 +1,62 @@ +"""Config flow to configure the Ambient PWS component.""" +from aioambient import Client +from aioambient.errors import AmbientError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY +from homeassistant.helpers import aiohttp_client + +from .const import CONF_APP_KEY, DOMAIN + + +class AmbientStationFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle an Ambient PWS config flow.""" + + VERSION = 2 + + def __init__(self): + """Initialize the config flow.""" + self.data_schema = vol.Schema( + {vol.Required(CONF_API_KEY): str, vol.Required(CONF_APP_KEY): str} + ) + + async def _show_form(self, errors=None): + """Show the form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=self.data_schema, + errors=errors if errors else {}, + ) + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + return await self.async_step_user(import_config) + + async def async_step_user(self, user_input=None): + """Handle the start of the config flow.""" + if not user_input: + return await self._show_form() + + await self.async_set_unique_id(user_input[CONF_APP_KEY]) + self._abort_if_unique_id_configured() + + session = aiohttp_client.async_get_clientsession(self.hass) + client = Client( + user_input[CONF_API_KEY], user_input[CONF_APP_KEY], session=session + ) + + try: + devices = await client.api.get_devices() + except AmbientError: + return await self._show_form({"base": "invalid_key"}) + + if not devices: + return await self._show_form({"base": "no_devices"}) + + # The Application Key (which identifies each config entry) is too long + # to show nicely in the UI, so we take the first 12 characters (similar + # to how GitHub does it): + return self.async_create_entry( + title=user_input[CONF_APP_KEY][:12], data=user_input + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/const.py new file mode 100644 index 00000000000..87b5ff61877 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/const.py @@ -0,0 +1,12 @@ +"""Define constants for the Ambient PWS component.""" +import logging + +DOMAIN = "ambient_station" +LOGGER = logging.getLogger(__package__) + +ATTR_LAST_DATA = "last_data" +ATTR_MONITORED_CONDITIONS = "monitored_conditions" + +CONF_APP_KEY = "app_key" + +DATA_CLIENT = "data_client" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/manifest.json new file mode 100644 index 00000000000..6d4c40d260d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "ambient_station", + "name": "Ambient Weather Station", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ambient_station", + "requirements": ["aioambient==1.2.4"], + "codeowners": ["@bachya"], + "iot_class": "cloud_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/sensor.py new file mode 100644 index 00000000000..7c60d1da9bc --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/sensor.py @@ -0,0 +1,87 @@ +"""Support for Ambient Weather Station sensors.""" +from homeassistant.components.sensor import DOMAIN as SENSOR, SensorEntity +from homeassistant.const import ATTR_NAME +from homeassistant.core import callback + +from . import ( + SENSOR_TYPES, + TYPE_SOLARRADIATION, + TYPE_SOLARRADIATION_LX, + AmbientWeatherEntity, +) +from .const import ATTR_LAST_DATA, ATTR_MONITORED_CONDITIONS, DATA_CLIENT, DOMAIN + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Ambient PWS sensors based on a config entry.""" + ambient = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + + sensor_list = [] + for mac_address, station in ambient.stations.items(): + for condition in station[ATTR_MONITORED_CONDITIONS]: + name, unit, kind, device_class = SENSOR_TYPES[condition] + if kind == SENSOR: + sensor_list.append( + AmbientWeatherSensor( + ambient, + mac_address, + station[ATTR_NAME], + condition, + name, + device_class, + unit, + ) + ) + + async_add_entities(sensor_list, True) + + +class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity): + """Define an Ambient sensor.""" + + def __init__( + self, + ambient, + mac_address, + station_name, + sensor_type, + sensor_name, + device_class, + unit, + ): + """Initialize the sensor.""" + super().__init__( + ambient, mac_address, station_name, sensor_type, sensor_name, device_class + ) + + self._unit = unit + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit + + @callback + def update_from_latest_data(self): + """Fetch new state data for the sensor.""" + if self._sensor_type == TYPE_SOLARRADIATION_LX: + # If the user requests the solarradiation_lx sensor, use the + # value of the solarradiation sensor and apply a very accurate + # approximation of converting sunlight W/m^2 to lx: + w_m2_brightness_val = self._ambient.stations[self._mac_address][ + ATTR_LAST_DATA + ].get(TYPE_SOLARRADIATION) + + if w_m2_brightness_val is None: + self._state = None + else: + self._state = round(float(w_m2_brightness_val) / 0.0079) + else: + self._state = self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get( + self._sensor_type + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/strings.json new file mode 100644 index 00000000000..a9bce82e10b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "title": "Fill in your information", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "app_key": "Application Key" + } + } + }, + "error": { + "invalid_key": "[%key:common::config_flow::error::invalid_api_key%]", + "no_devices": "No devices found in account" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/bg.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/bg.json new file mode 100644 index 00000000000..173b1c39c5f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/bg.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447 \u0438/\u0438\u043b\u0438 Application \u043a\u043b\u044e\u0447", + "no_devices": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043f\u0440\u043e\u0444\u0438\u043b\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "app_key": "Application \u043a\u043b\u044e\u0447" + }, + "title": "\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f\u0442\u0430 \u0441\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/ca.json new file mode 100644 index 00000000000..31a32da995e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, + "error": { + "invalid_key": "Clau API inv\u00e0lida", + "no_devices": "No s'ha trobat cap dispositiu al compte" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "app_key": "Clau d'aplicaci\u00f3" + }, + "title": "Introdueix la teva informaci\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/cs.json new file mode 100644 index 00000000000..425bc3ba191 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena" + }, + "error": { + "invalid_key": "Neplatn\u00fd kl\u00ed\u010d API", + "no_devices": "V \u00fa\u010dtu nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "app_key": "Kl\u00ed\u010d aplikace" + }, + "title": "Vypl\u0148te sv\u00e9 \u00fadaje" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/da.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/da.json new file mode 100644 index 00000000000..b8a4f1ab29e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/da.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Denne appn\u00f8gle er allerede i brug." + }, + "error": { + "invalid_key": "Ugyldig API n\u00f8gle og/eller applikationsn\u00f8gle", + "no_devices": "Ingen enheder fundet i konto" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8gle", + "app_key": "Applikationsn\u00f8gle" + }, + "title": "Udfyld dine oplysninger" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/de.json new file mode 100644 index 00000000000..c6570fee0e3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, + "error": { + "invalid_key": "Ung\u00fcltiger API-Schl\u00fcssel", + "no_devices": "Keine Ger\u00e4te im Konto gefunden" + }, + "step": { + "user": { + "data": { + "api_key": "API Schl\u00fcssel", + "app_key": "Anwendungsschl\u00fcssel" + }, + "title": "Gib deine Informationen ein" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/en.json new file mode 100644 index 00000000000..45e01462fc5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured" + }, + "error": { + "invalid_key": "Invalid API key", + "no_devices": "No devices found in account" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "app_key": "Application Key" + }, + "title": "Fill in your information" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/es-419.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/es-419.json new file mode 100644 index 00000000000..b16c5af9c62 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/es-419.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Esta clave de aplicaci\u00f3n ya est\u00e1 en uso." + }, + "error": { + "invalid_key": "Clave de API y/o clave de aplicaci\u00f3n no v\u00e1lida", + "no_devices": "No se han encontrado dispositivos en la cuenta." + }, + "step": { + "user": { + "data": { + "api_key": "Clave API", + "app_key": "Clave de aplicaci\u00f3n" + }, + "title": "Completa tu informaci\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/es.json new file mode 100644 index 00000000000..90e93e519b7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado" + }, + "error": { + "invalid_key": "Clave API no v\u00e1lida", + "no_devices": "No se han encontrado dispositivos en la cuenta" + }, + "step": { + "user": { + "data": { + "api_key": "Clave API", + "app_key": "Clave de aplicaci\u00f3n" + }, + "title": "Completa tu informaci\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/et.json new file mode 100644 index 00000000000..211e55dd0cf --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud" + }, + "error": { + "invalid_key": "Vale API v\u00f5ti", + "no_devices": "Kontolt ei leitud \u00fchtegi seadet" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "app_key": "API v\u00f5ti" + }, + "title": "Sisesta oma teave" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/fi.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/fi.json new file mode 100644 index 00000000000..acb097c2d7d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/fi.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API-avain", + "app_key": "Sovellusavain" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/fr.json new file mode 100644 index 00000000000..d88e9f9c9f6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cette cl\u00e9 d'application est d\u00e9j\u00e0 utilis\u00e9e." + }, + "error": { + "invalid_key": "Cl\u00e9 d'API et / ou cl\u00e9 d'application non valide", + "no_devices": "Aucun appareil trouv\u00e9 dans le compte" + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "app_key": "Cl\u00e9 d'application" + }, + "title": "Veuillez saisir vos informations" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/he.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/he.json new file mode 100644 index 00000000000..f5afbca71c0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "no_devices": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05df \u05d1\u05d7\u05e9\u05d1\u05d5\u05df" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + }, + "title": "\u05de\u05dc\u05d0 \u05d0\u05ea \u05d4\u05e4\u05e8\u05d8\u05d9\u05dd \u05e9\u05dc\u05da" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/hu.json new file mode 100644 index 00000000000..7c7e3a658b9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "invalid_key": "\u00c9rv\u00e9nytelen API kulcs", + "no_devices": "Nincs a fi\u00f3kodban tal\u00e1lhat\u00f3 eszk\u00f6z" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "app_key": "Alkalmaz\u00e1skulcs" + }, + "title": "T\u00f6ltsd ki az adataid" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/id.json new file mode 100644 index 00000000000..1b5a1dd0b21 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "error": { + "invalid_key": "Kunci API tidak valid", + "no_devices": "Tidak ada perangkat yang ditemukan dalam akun" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "app_key": "Kunci Aplikasi" + }, + "title": "Isi informasi Anda" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/it.json new file mode 100644 index 00000000000..8984314349c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + }, + "error": { + "invalid_key": "Chiave API non valida", + "no_devices": "Nessun dispositivo trovato nell'account" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "app_key": "Application Key" + }, + "title": "Inserisci i tuoi dati" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/ko.json new file mode 100644 index 00000000000..6fc8f4b17fc --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "invalid_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "no_devices": "\uacc4\uc815\uc5d0 \uae30\uae30\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "api_key": "API \ud0a4", + "app_key": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ud0a4" + }, + "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4 \uc785\ub825\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/lb.json new file mode 100644 index 00000000000..f565639ac7d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/lb.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Service ass scho konfigur\u00e9iert" + }, + "error": { + "invalid_key": "Ong\u00ebltegen API Schl\u00ebssel", + "no_devices": "Keng Apparater am Kont fonnt" + }, + "step": { + "user": { + "data": { + "api_key": "API Schl\u00ebssel", + "app_key": "Applikatioun's Schl\u00ebssel" + }, + "title": "F\u00ebllt \u00e4r Informatiounen aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/nl.json new file mode 100644 index 00000000000..008bc10e084 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Service is al geconfigureerd" + }, + "error": { + "invalid_key": "Ongeldige API-sleutel", + "no_devices": "Geen apparaten gevonden in account" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel", + "app_key": "Applicatiesleutel" + }, + "title": "Vul uw gegevens in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/no.json new file mode 100644 index 00000000000..cf471f2c3a1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, + "error": { + "invalid_key": "Ugyldig API-n\u00f8kkel", + "no_devices": "Ingen enheter funnet i kontoen" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "app_key": "Applikasjonsn\u00f8kkel" + }, + "title": "Fyll ut informasjonen din" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/pl.json new file mode 100644 index 00000000000..3fc36d82840 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + }, + "error": { + "invalid_key": "Nieprawid\u0142owy klucz API", + "no_devices": "Nie znaleziono urz\u0105dze\u0144 na koncie" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "app_key": "Klucz aplikacji" + }, + "title": "Wprowad\u017a dane" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/pt-BR.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/pt-BR.json new file mode 100644 index 00000000000..d3ac36bf0e2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_key": "Chave de API e / ou chave de aplicativo inv\u00e1lidas", + "no_devices": "Nenhum dispositivo encontrado na conta" + }, + "step": { + "user": { + "data": { + "api_key": "Chave API", + "app_key": "Chave de aplicativo" + }, + "title": "Preencha suas informa\u00e7\u00f5es" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/pt.json new file mode 100644 index 00000000000..c67faa25f0b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_key": "Chave de API e/ou chave de aplica\u00e7\u00e3o inv\u00e1lidas", + "no_devices": "Nenhum dispositivo encontrado na conta" + }, + "step": { + "user": { + "data": { + "api_key": "Chave de API", + "app_key": "Chave de aplica\u00e7\u00e3o" + }, + "title": "Preencha as suas informa\u00e7\u00f5es" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/ru.json new file mode 100644 index 00000000000..83074006641 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "no_devices": "\u0412 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "app_key": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f" + }, + "title": "Ambient PWS" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/sl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/sl.json new file mode 100644 index 00000000000..0fbacf5ccc1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/sl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ta klju\u010d za aplikacijo je \u017ee v uporabi." + }, + "error": { + "invalid_key": "Neveljaven klju\u010d API in / ali klju\u010d aplikacije", + "no_devices": "V ra\u010dunu ni najdene nobene naprave" + }, + "step": { + "user": { + "data": { + "api_key": "API Klju\u010d", + "app_key": "Klju\u010d aplikacije" + }, + "title": "Izpolnite svoje podatke" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/sv.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/sv.json new file mode 100644 index 00000000000..7c6be84d594 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_key": "Ogiltigt API-nyckel och/eller applikationsnyckel", + "no_devices": "Inga enheter hittades i kontot" + }, + "step": { + "user": { + "data": { + "api_key": "API-nyckel", + "app_key": "Applikationsnyckel" + }, + "title": "Fyll i dina uppgifter" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/th.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/th.json new file mode 100644 index 00000000000..a6115413edc --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/th.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "no_devices": "\u0e44\u0e21\u0e48\u0e1e\u0e1a\u0e2d\u0e38\u0e1b\u0e01\u0e23\u0e13\u0e4c\u0e43\u0e14\u0e46 \u0e43\u0e19\u0e1a\u0e31\u0e0d\u0e0a\u0e35\u0e40\u0e25\u0e22" + }, + "step": { + "user": { + "data": { + "api_key": "\u0e04\u0e35\u0e22\u0e4c API", + "app_key": "\u0e23\u0e2b\u0e31\u0e2a\u0e41\u0e2d\u0e1b\u0e1e\u0e25\u0e34\u0e40\u0e04\u0e0a\u0e31\u0e19" + }, + "title": "\u0e01\u0e23\u0e2d\u0e01\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25\u0e02\u0e2d\u0e07\u0e04\u0e38\u0e13" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/tr.json new file mode 100644 index 00000000000..908d97f5758 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_key": "Ge\u00e7ersiz API anahtar\u0131" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/uk.json new file mode 100644 index 00000000000..722cf99af7e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "invalid_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API", + "no_devices": "\u0412 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441\u0456 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "app_key": "\u041a\u043b\u044e\u0447 \u0434\u043e\u0434\u0430\u0442\u043a\u0443" + }, + "title": "Ambient PWS" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/zh-Hans.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/zh-Hans.json new file mode 100644 index 00000000000..fc092c7c247 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/zh-Hans.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_key": "\u65e0\u6548\u7684 API \u5bc6\u94a5\u548c/\u6216 Application Key", + "no_devices": "\u6ca1\u6709\u5728\u5e10\u6237\u4e2d\u627e\u5230\u8bbe\u5907" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "app_key": "Application Key" + }, + "title": "\u586b\u5199\u60a8\u7684\u4fe1\u606f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/zh-Hant.json new file mode 100644 index 00000000000..dab15def7b4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ambient_station/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "invalid_key": "API \u5bc6\u9470\u7121\u6548", + "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u88dd\u7f6e" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470", + "app_key": "\u61c9\u7528\u5bc6\u9470" + }, + "title": "\u586b\u5beb\u8cc7\u8a0a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/__init__.py new file mode 100644 index 00000000000..8d274f12044 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/__init__.py @@ -0,0 +1,359 @@ +"""Support for Amcrest IP cameras.""" +from contextlib import suppress +from datetime import timedelta +import logging +import threading + +import aiohttp +from amcrest import AmcrestError, Http, LoginError +import voluptuous as vol + +from homeassistant.auth.permissions.const import POLICY_CONTROL +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR +from homeassistant.components.camera import DOMAIN as CAMERA +from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_AUTHENTICATION, + CONF_BINARY_SENSORS, + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_SENSORS, + CONF_USERNAME, + ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, + HTTP_BASIC_AUTHENTICATION, +) +from homeassistant.exceptions import Unauthorized, UnknownUser +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send +from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.service import async_extract_entity_ids + +from .binary_sensor import BINARY_POLLED_SENSORS, BINARY_SENSORS, check_binary_sensors +from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST +from .const import ( + CAMERAS, + COMM_RETRIES, + COMM_TIMEOUT, + DATA_AMCREST, + DEVICES, + DOMAIN, + SENSOR_EVENT_CODE, + SERVICE_EVENT, + SERVICE_UPDATE, +) +from .helpers import service_signal +from .sensor import SENSORS + +_LOGGER = logging.getLogger(__name__) + +CONF_RESOLUTION = "resolution" +CONF_STREAM_SOURCE = "stream_source" +CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" +CONF_CONTROL_LIGHT = "control_light" + +DEFAULT_NAME = "Amcrest Camera" +DEFAULT_PORT = 80 +DEFAULT_RESOLUTION = "high" +DEFAULT_ARGUMENTS = "-pred 1" +MAX_ERRORS = 5 +RECHECK_INTERVAL = timedelta(minutes=1) + +NOTIFICATION_ID = "amcrest_notification" +NOTIFICATION_TITLE = "Amcrest Camera Setup" + +RESOLUTION_LIST = {"high": 0, "low": 1} + +SCAN_INTERVAL = timedelta(seconds=10) + +AUTHENTICATION_LIST = {"basic": "basic"} + + +def _has_unique_names(devices): + names = [device[CONF_NAME] for device in devices] + vol.Schema(vol.Unique())(names) + return devices + + +AMCREST_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): vol.All( + vol.In(AUTHENTICATION_LIST) + ), + vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION): vol.All( + vol.In(RESOLUTION_LIST) + ), + vol.Optional(CONF_STREAM_SOURCE, default=STREAM_SOURCE_LIST[0]): vol.All( + vol.In(STREAM_SOURCE_LIST) + ), + vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, + vol.Optional(CONF_BINARY_SENSORS): vol.All( + cv.ensure_list, [vol.In(BINARY_SENSORS)], vol.Unique(), check_binary_sensors + ), + vol.Optional(CONF_SENSORS): vol.All( + cv.ensure_list, [vol.In(SENSORS)], vol.Unique() + ), + vol.Optional(CONF_CONTROL_LIGHT, default=True): cv.boolean, + } +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.All(cv.ensure_list, [AMCREST_SCHEMA], _has_unique_names)}, + extra=vol.ALLOW_EXTRA, +) + + +class AmcrestChecker(Http): + """amcrest.Http wrapper for catching errors.""" + + def __init__(self, hass, name, host, port, user, password): + """Initialize.""" + self._hass = hass + self._wrap_name = name + self._wrap_errors = 0 + self._wrap_lock = threading.Lock() + self._wrap_login_err = False + self._wrap_event_flag = threading.Event() + self._wrap_event_flag.set() + self._unsub_recheck = None + super().__init__( + host, + port, + user, + password, + retries_connection=COMM_RETRIES, + timeout_protocol=COMM_TIMEOUT, + ) + + @property + def available(self): + """Return if camera's API is responding.""" + return self._wrap_errors <= MAX_ERRORS and not self._wrap_login_err + + @property + def available_flag(self): + """Return threading event flag that indicates if camera's API is responding.""" + return self._wrap_event_flag + + def _start_recovery(self): + self._wrap_event_flag.clear() + dispatcher_send(self._hass, service_signal(SERVICE_UPDATE, self._wrap_name)) + self._unsub_recheck = track_time_interval( + self._hass, self._wrap_test_online, RECHECK_INTERVAL + ) + + def command(self, *args, **kwargs): + """amcrest.Http.command wrapper to catch errors.""" + try: + ret = super().command(*args, **kwargs) + except LoginError as ex: + with self._wrap_lock: + was_online = self.available + was_login_err = self._wrap_login_err + self._wrap_login_err = True + if not was_login_err: + _LOGGER.error("%s camera offline: Login error: %s", self._wrap_name, ex) + if was_online: + self._start_recovery() + raise + except AmcrestError: + with self._wrap_lock: + was_online = self.available + errs = self._wrap_errors = self._wrap_errors + 1 + offline = not self.available + _LOGGER.debug("%s camera errs: %i", self._wrap_name, errs) + if was_online and offline: + _LOGGER.error("%s camera offline: Too many errors", self._wrap_name) + self._start_recovery() + raise + with self._wrap_lock: + was_offline = not self.available + self._wrap_errors = 0 + self._wrap_login_err = False + if was_offline: + self._unsub_recheck() + self._unsub_recheck = None + _LOGGER.error("%s camera back online", self._wrap_name) + self._wrap_event_flag.set() + dispatcher_send(self._hass, service_signal(SERVICE_UPDATE, self._wrap_name)) + return ret + + def _wrap_test_online(self, now): + """Test if camera is back online.""" + _LOGGER.debug("Testing if %s back online", self._wrap_name) + with suppress(AmcrestError): + self.current_time # pylint: disable=pointless-statement + + +def _monitor_events(hass, name, api, event_codes): + event_codes = set(event_codes) + while True: + api.available_flag.wait() + try: + for code, payload in api.event_actions("All", retries=5): + event_data = {"camera": name, "event": code, "payload": payload} + hass.bus.fire("amcrest", event_data) + if code in event_codes: + signal = service_signal(SERVICE_EVENT, name, code) + start = any( + str(key).lower() == "action" and str(val).lower() == "start" + for key, val in payload.items() + ) + _LOGGER.debug("Sending signal: '%s': %s", signal, start) + dispatcher_send(hass, signal, start) + except AmcrestError as error: + _LOGGER.warning( + "Error while processing events from %s camera: %r", name, error + ) + + +def _start_event_monitor(hass, name, api, event_codes): + thread = threading.Thread( + target=_monitor_events, + name=f"Amcrest {name}", + args=(hass, name, api, event_codes), + daemon=True, + ) + thread.start() + + +def setup(hass, config): + """Set up the Amcrest IP Camera component.""" + hass.data.setdefault(DATA_AMCREST, {DEVICES: {}, CAMERAS: []}) + + for device in config[DOMAIN]: + name = device[CONF_NAME] + username = device[CONF_USERNAME] + password = device[CONF_PASSWORD] + + api = AmcrestChecker( + hass, name, device[CONF_HOST], device[CONF_PORT], username, password + ) + + ffmpeg_arguments = device[CONF_FFMPEG_ARGUMENTS] + resolution = RESOLUTION_LIST[device[CONF_RESOLUTION]] + binary_sensors = device.get(CONF_BINARY_SENSORS) + sensors = device.get(CONF_SENSORS) + stream_source = device[CONF_STREAM_SOURCE] + control_light = device.get(CONF_CONTROL_LIGHT) + + # currently aiohttp only works with basic authentication + # only valid for mjpeg streaming + if device[CONF_AUTHENTICATION] == HTTP_BASIC_AUTHENTICATION: + authentication = aiohttp.BasicAuth(username, password) + else: + authentication = None + + hass.data[DATA_AMCREST][DEVICES][name] = AmcrestDevice( + api, + authentication, + ffmpeg_arguments, + stream_source, + resolution, + control_light, + ) + + discovery.load_platform(hass, CAMERA, DOMAIN, {CONF_NAME: name}, config) + + event_codes = [] + if binary_sensors: + discovery.load_platform( + hass, + BINARY_SENSOR, + DOMAIN, + {CONF_NAME: name, CONF_BINARY_SENSORS: binary_sensors}, + config, + ) + event_codes = [ + BINARY_SENSORS[sensor_type][SENSOR_EVENT_CODE] + for sensor_type in binary_sensors + if sensor_type not in BINARY_POLLED_SENSORS + ] + + _start_event_monitor(hass, name, api, event_codes) + + if sensors: + discovery.load_platform( + hass, SENSOR, DOMAIN, {CONF_NAME: name, CONF_SENSORS: sensors}, config + ) + + if not hass.data[DATA_AMCREST][DEVICES]: + return False + + def have_permission(user, entity_id): + return not user or user.permissions.check_entity(entity_id, POLICY_CONTROL) + + async def async_extract_from_service(call): + if call.context.user_id: + user = await hass.auth.async_get_user(call.context.user_id) + if user is None: + raise UnknownUser(context=call.context) + else: + user = None + + if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL: + # Return all entity_ids user has permission to control. + return [ + entity_id + for entity_id in hass.data[DATA_AMCREST][CAMERAS] + if have_permission(user, entity_id) + ] + + if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE: + return [] + + call_ids = await async_extract_entity_ids(hass, call) + entity_ids = [] + for entity_id in hass.data[DATA_AMCREST][CAMERAS]: + if entity_id not in call_ids: + continue + if not have_permission(user, entity_id): + raise Unauthorized( + context=call.context, entity_id=entity_id, permission=POLICY_CONTROL + ) + entity_ids.append(entity_id) + return entity_ids + + async def async_service_handler(call): + args = [] + for arg in CAMERA_SERVICES[call.service][2]: + args.append(call.data[arg]) + for entity_id in await async_extract_from_service(call): + async_dispatcher_send(hass, service_signal(call.service, entity_id), *args) + + for service, params in CAMERA_SERVICES.items(): + hass.services.register(DOMAIN, service, async_service_handler, params[0]) + + return True + + +class AmcrestDevice: + """Representation of a base Amcrest discovery device.""" + + def __init__( + self, + api, + authentication, + ffmpeg_arguments, + stream_source, + resolution, + control_light, + ): + """Initialize the entity.""" + self.api = api + self.authentication = authentication + self.ffmpeg_arguments = ffmpeg_arguments + self.stream_source = stream_source + self.resolution = resolution + self.control_light = control_light diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/binary_sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/binary_sensor.py new file mode 100644 index 00000000000..0add382b81f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/binary_sensor.py @@ -0,0 +1,211 @@ +"""Support for Amcrest IP camera binary sensors.""" +from contextlib import suppress +from datetime import timedelta +import logging + +from amcrest import AmcrestError +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_SOUND, + BinarySensorEntity, +) +from homeassistant.const import CONF_BINARY_SENSORS, CONF_NAME +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util import Throttle + +from .const import ( + BINARY_SENSOR_SCAN_INTERVAL_SECS, + DATA_AMCREST, + DEVICES, + SENSOR_DEVICE_CLASS, + SENSOR_EVENT_CODE, + SENSOR_NAME, + SERVICE_EVENT, + SERVICE_UPDATE, +) +from .helpers import log_update_error, service_signal + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=BINARY_SENSOR_SCAN_INTERVAL_SECS) +_ONLINE_SCAN_INTERVAL = timedelta(seconds=60 - BINARY_SENSOR_SCAN_INTERVAL_SECS) + +BINARY_SENSOR_AUDIO_DETECTED = "audio_detected" +BINARY_SENSOR_AUDIO_DETECTED_POLLED = "audio_detected_polled" +BINARY_SENSOR_MOTION_DETECTED = "motion_detected" +BINARY_SENSOR_MOTION_DETECTED_POLLED = "motion_detected_polled" +BINARY_SENSOR_ONLINE = "online" +BINARY_SENSOR_CROSSLINE_DETECTED = "crossline_detected" +BINARY_SENSOR_CROSSLINE_DETECTED_POLLED = "crossline_detected_polled" +BINARY_POLLED_SENSORS = [ + BINARY_SENSOR_AUDIO_DETECTED_POLLED, + BINARY_SENSOR_MOTION_DETECTED_POLLED, + BINARY_SENSOR_ONLINE, +] +_AUDIO_DETECTED_PARAMS = ("Audio Detected", DEVICE_CLASS_SOUND, "AudioMutation") +_MOTION_DETECTED_PARAMS = ("Motion Detected", DEVICE_CLASS_MOTION, "VideoMotion") +_CROSSLINE_DETECTED_PARAMS = ( + "CrossLine Detected", + DEVICE_CLASS_MOTION, + "CrossLineDetection", +) +BINARY_SENSORS = { + BINARY_SENSOR_AUDIO_DETECTED: _AUDIO_DETECTED_PARAMS, + BINARY_SENSOR_AUDIO_DETECTED_POLLED: _AUDIO_DETECTED_PARAMS, + BINARY_SENSOR_MOTION_DETECTED: _MOTION_DETECTED_PARAMS, + BINARY_SENSOR_MOTION_DETECTED_POLLED: _MOTION_DETECTED_PARAMS, + BINARY_SENSOR_CROSSLINE_DETECTED: _CROSSLINE_DETECTED_PARAMS, + BINARY_SENSOR_CROSSLINE_DETECTED_POLLED: _CROSSLINE_DETECTED_PARAMS, + BINARY_SENSOR_ONLINE: ("Online", DEVICE_CLASS_CONNECTIVITY, None), +} +BINARY_SENSORS = { + k: dict(zip((SENSOR_NAME, SENSOR_DEVICE_CLASS, SENSOR_EVENT_CODE), v)) + for k, v in BINARY_SENSORS.items() +} +_EXCLUSIVE_OPTIONS = [ + {BINARY_SENSOR_MOTION_DETECTED, BINARY_SENSOR_MOTION_DETECTED_POLLED}, + {BINARY_SENSOR_CROSSLINE_DETECTED, BINARY_SENSOR_CROSSLINE_DETECTED_POLLED}, +] + +_UPDATE_MSG = "Updating %s binary sensor" + + +def check_binary_sensors(value): + """Validate binary sensor configurations.""" + for exclusive_options in _EXCLUSIVE_OPTIONS: + if len(set(value) & exclusive_options) > 1: + raise vol.Invalid( + f"must contain at most one of {', '.join(exclusive_options)}." + ) + return value + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up a binary sensor for an Amcrest IP Camera.""" + if discovery_info is None: + return + + name = discovery_info[CONF_NAME] + device = hass.data[DATA_AMCREST][DEVICES][name] + async_add_entities( + [ + AmcrestBinarySensor(name, device, sensor_type) + for sensor_type in discovery_info[CONF_BINARY_SENSORS] + ], + True, + ) + + +class AmcrestBinarySensor(BinarySensorEntity): + """Binary sensor for Amcrest camera.""" + + def __init__(self, name, device, sensor_type): + """Initialize entity.""" + self._name = f"{name} {BINARY_SENSORS[sensor_type][SENSOR_NAME]}" + self._signal_name = name + self._api = device.api + self._sensor_type = sensor_type + self._state = None + self._device_class = BINARY_SENSORS[sensor_type][SENSOR_DEVICE_CLASS] + self._event_code = BINARY_SENSORS[sensor_type][SENSOR_EVENT_CODE] + self._unsub_dispatcher = [] + + @property + def should_poll(self): + """Return True if entity has to be polled for state.""" + return self._sensor_type in BINARY_POLLED_SENSORS + + @property + def name(self): + """Return entity name.""" + return self._name + + @property + def is_on(self): + """Return if entity is on.""" + return self._state + + @property + def device_class(self): + """Return device class.""" + return self._device_class + + @property + def available(self): + """Return True if entity is available.""" + return self._sensor_type == BINARY_SENSOR_ONLINE or self._api.available + + def update(self): + """Update entity.""" + if self._sensor_type == BINARY_SENSOR_ONLINE: + self._update_online() + else: + self._update_others() + + @Throttle(_ONLINE_SCAN_INTERVAL) + def _update_online(self): + if not (self._api.available or self.is_on): + return + _LOGGER.debug(_UPDATE_MSG, self._name) + if self._api.available: + # Send a command to the camera to test if we can still communicate with it. + # Override of Http.command() in __init__.py will set self._api.available + # accordingly. + with suppress(AmcrestError): + self._api.current_time # pylint: disable=pointless-statement + self._state = self._api.available + + def _update_others(self): + if not self.available: + return + _LOGGER.debug(_UPDATE_MSG, self._name) + + try: + self._state = "channels" in self._api.event_channels_happened( + self._event_code + ) + except AmcrestError as error: + log_update_error(_LOGGER, "update", self.name, "binary sensor", error) + + async def async_on_demand_update(self): + """Update state.""" + if self._sensor_type == BINARY_SENSOR_ONLINE: + _LOGGER.debug(_UPDATE_MSG, self._name) + self._state = self._api.available + self.async_write_ha_state() + return + self.async_schedule_update_ha_state(True) + + @callback + def async_event_received(self, start): + """Update state from received event.""" + _LOGGER.debug(_UPDATE_MSG, self._name) + self._state = start + self.async_write_ha_state() + + async def async_added_to_hass(self): + """Subscribe to signals.""" + self._unsub_dispatcher.append( + async_dispatcher_connect( + self.hass, + service_signal(SERVICE_UPDATE, self._signal_name), + self.async_on_demand_update, + ) + ) + if self._event_code and self._sensor_type not in BINARY_POLLED_SENSORS: + self._unsub_dispatcher.append( + async_dispatcher_connect( + self.hass, + service_signal(SERVICE_EVENT, self._signal_name, self._event_code), + self.async_event_received, + ) + ) + + async def async_will_remove_from_hass(self): + """Disconnect from update signal.""" + for unsub_dispatcher in self._unsub_dispatcher: + unsub_dispatcher() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/camera.py b/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/camera.py new file mode 100644 index 00000000000..92453d24144 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/camera.py @@ -0,0 +1,597 @@ +"""Support for Amcrest IP cameras.""" +import asyncio +from datetime import timedelta +from functools import partial +import logging + +from amcrest import AmcrestError +from haffmpeg.camera import CameraMjpeg +import voluptuous as vol + +from homeassistant.components.camera import SUPPORT_ON_OFF, SUPPORT_STREAM, Camera +from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.helpers.aiohttp_client import ( + async_aiohttp_proxy_stream, + async_aiohttp_proxy_web, + async_get_clientsession, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import ( + CAMERA_WEB_SESSION_TIMEOUT, + CAMERAS, + COMM_TIMEOUT, + DATA_AMCREST, + DEVICES, + SERVICE_UPDATE, + SNAPSHOT_TIMEOUT, +) +from .helpers import log_update_error, service_signal + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=15) + +STREAM_SOURCE_LIST = ["snapshot", "mjpeg", "rtsp"] + +_SRV_EN_REC = "enable_recording" +_SRV_DS_REC = "disable_recording" +_SRV_EN_AUD = "enable_audio" +_SRV_DS_AUD = "disable_audio" +_SRV_EN_MOT_REC = "enable_motion_recording" +_SRV_DS_MOT_REC = "disable_motion_recording" +_SRV_GOTO = "goto_preset" +_SRV_CBW = "set_color_bw" +_SRV_TOUR_ON = "start_tour" +_SRV_TOUR_OFF = "stop_tour" + +_SRV_PTZ_CTRL = "ptz_control" +_ATTR_PTZ_TT = "travel_time" +_ATTR_PTZ_MOV = "movement" +_MOV = [ + "zoom_out", + "zoom_in", + "right", + "left", + "up", + "down", + "right_down", + "right_up", + "left_down", + "left_up", +] +_ZOOM_ACTIONS = ["ZoomWide", "ZoomTele"] +_MOVE_1_ACTIONS = ["Right", "Left", "Up", "Down"] +_MOVE_2_ACTIONS = ["RightDown", "RightUp", "LeftDown", "LeftUp"] +_ACTION = _ZOOM_ACTIONS + _MOVE_1_ACTIONS + _MOVE_2_ACTIONS + +_DEFAULT_TT = 0.2 + +_ATTR_PRESET = "preset" +_ATTR_COLOR_BW = "color_bw" + +_CBW_COLOR = "color" +_CBW_AUTO = "auto" +_CBW_BW = "bw" +_CBW = [_CBW_COLOR, _CBW_AUTO, _CBW_BW] + +_SRV_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids}) +_SRV_GOTO_SCHEMA = _SRV_SCHEMA.extend( + {vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1))} +) +_SRV_CBW_SCHEMA = _SRV_SCHEMA.extend({vol.Required(_ATTR_COLOR_BW): vol.In(_CBW)}) +_SRV_PTZ_SCHEMA = _SRV_SCHEMA.extend( + { + vol.Required(_ATTR_PTZ_MOV): vol.In(_MOV), + vol.Optional(_ATTR_PTZ_TT, default=_DEFAULT_TT): cv.small_float, + } +) + +CAMERA_SERVICES = { + _SRV_EN_REC: (_SRV_SCHEMA, "async_enable_recording", ()), + _SRV_DS_REC: (_SRV_SCHEMA, "async_disable_recording", ()), + _SRV_EN_AUD: (_SRV_SCHEMA, "async_enable_audio", ()), + _SRV_DS_AUD: (_SRV_SCHEMA, "async_disable_audio", ()), + _SRV_EN_MOT_REC: (_SRV_SCHEMA, "async_enable_motion_recording", ()), + _SRV_DS_MOT_REC: (_SRV_SCHEMA, "async_disable_motion_recording", ()), + _SRV_GOTO: (_SRV_GOTO_SCHEMA, "async_goto_preset", (_ATTR_PRESET,)), + _SRV_CBW: (_SRV_CBW_SCHEMA, "async_set_color_bw", (_ATTR_COLOR_BW,)), + _SRV_TOUR_ON: (_SRV_SCHEMA, "async_start_tour", ()), + _SRV_TOUR_OFF: (_SRV_SCHEMA, "async_stop_tour", ()), + _SRV_PTZ_CTRL: ( + _SRV_PTZ_SCHEMA, + "async_ptz_control", + (_ATTR_PTZ_MOV, _ATTR_PTZ_TT), + ), +} + +_BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF} + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up an Amcrest IP Camera.""" + if discovery_info is None: + return + + name = discovery_info[CONF_NAME] + device = hass.data[DATA_AMCREST][DEVICES][name] + async_add_entities([AmcrestCam(name, device, hass.data[DATA_FFMPEG])], True) + + +class CannotSnapshot(Exception): + """Conditions are not valid for taking a snapshot.""" + + +class AmcrestCommandFailed(Exception): + """Amcrest camera command did not work.""" + + +class AmcrestCam(Camera): + """An implementation of an Amcrest IP camera.""" + + def __init__(self, name, device, ffmpeg): + """Initialize an Amcrest camera.""" + super().__init__() + self._name = name + self._api = device.api + self._ffmpeg = ffmpeg + self._ffmpeg_arguments = device.ffmpeg_arguments + self._stream_source = device.stream_source + self._resolution = device.resolution + self._token = self._auth = device.authentication + self._control_light = device.control_light + self._is_recording = False + self._motion_detection_enabled = None + self._brand = None + self._model = None + self._audio_enabled = None + self._motion_recording_enabled = None + self._color_bw = None + self._rtsp_url = None + self._snapshot_task = None + self._unsub_dispatcher = [] + self._update_succeeded = False + + def _check_snapshot_ok(self): + available = self.available + if not available or not self.is_on: + _LOGGER.warning( + "Attempt to take snapshot when %s camera is %s", + self.name, + "offline" if not available else "off", + ) + raise CannotSnapshot + + async def _async_get_image(self): + try: + # Send the request to snap a picture and return raw jpg data + # Snapshot command needs a much longer read timeout than other commands. + return await self.hass.async_add_executor_job( + partial( + self._api.snapshot, + timeout=(COMM_TIMEOUT, SNAPSHOT_TIMEOUT), + stream=False, + ) + ) + except AmcrestError as error: + log_update_error(_LOGGER, "get image from", self.name, "camera", error) + return None + finally: + self._snapshot_task = None + + async def async_camera_image(self): + """Return a still image response from the camera.""" + _LOGGER.debug("Take snapshot from %s", self._name) + try: + # Amcrest cameras only support one snapshot command at a time. + # Hence need to wait if a previous snapshot has not yet finished. + # Also need to check that camera is online and turned on before each wait + # and before initiating shapshot. + while self._snapshot_task: + self._check_snapshot_ok() + _LOGGER.debug("Waiting for previous snapshot from %s", self._name) + await self._snapshot_task + self._check_snapshot_ok() + # Run snapshot command in separate Task that can't be cancelled so + # 1) it's not possible to send another snapshot command while camera is + # still working on a previous one, and + # 2) someone will be around to catch any exceptions. + self._snapshot_task = self.hass.async_create_task(self._async_get_image()) + return await asyncio.shield(self._snapshot_task) + except CannotSnapshot: + return None + + async def handle_async_mjpeg_stream(self, request): + """Return an MJPEG stream.""" + # The snapshot implementation is handled by the parent class + if self._stream_source == "snapshot": + return await super().handle_async_mjpeg_stream(request) + + if not self.available: + _LOGGER.warning( + "Attempt to stream %s when %s camera is offline", + self._stream_source, + self.name, + ) + return None + + if self._stream_source == "mjpeg": + # stream an MJPEG image stream directly from the camera + websession = async_get_clientsession(self.hass) + streaming_url = self._api.mjpeg_url(typeno=self._resolution) + stream_coro = websession.get( + streaming_url, auth=self._token, timeout=CAMERA_WEB_SESSION_TIMEOUT + ) + + return await async_aiohttp_proxy_web(self.hass, request, stream_coro) + + # streaming via ffmpeg + + streaming_url = self._rtsp_url + stream = CameraMjpeg(self._ffmpeg.binary) + await stream.open_camera(streaming_url, extra_cmd=self._ffmpeg_arguments) + + try: + stream_reader = await stream.get_reader() + return await async_aiohttp_proxy_stream( + self.hass, + request, + stream_reader, + self._ffmpeg.ffmpeg_stream_content_type, + ) + finally: + await stream.close() + + # Entity property overrides + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + return True + + @property + def name(self): + """Return the name of this camera.""" + return self._name + + @property + def extra_state_attributes(self): + """Return the Amcrest-specific camera state attributes.""" + attr = {} + if self._audio_enabled is not None: + attr["audio"] = _BOOL_TO_STATE.get(self._audio_enabled) + if self._motion_recording_enabled is not None: + attr["motion_recording"] = _BOOL_TO_STATE.get( + self._motion_recording_enabled + ) + if self._color_bw is not None: + attr[_ATTR_COLOR_BW] = self._color_bw + return attr + + @property + def available(self): + """Return True if entity is available.""" + return self._api.available + + @property + def supported_features(self): + """Return supported features.""" + return SUPPORT_ON_OFF | SUPPORT_STREAM + + # Camera property overrides + + @property + def is_recording(self): + """Return true if the device is recording.""" + return self._is_recording + + @property + def brand(self): + """Return the camera brand.""" + return self._brand + + @property + def motion_detection_enabled(self): + """Return the camera motion detection status.""" + return self._motion_detection_enabled + + @property + def model(self): + """Return the camera model.""" + return self._model + + async def stream_source(self): + """Return the source of the stream.""" + return self._rtsp_url + + @property + def is_on(self): + """Return true if on.""" + return self.is_streaming + + # Other Entity method overrides + + async def async_on_demand_update(self): + """Update state.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Subscribe to signals and add camera to list.""" + for service, params in CAMERA_SERVICES.items(): + self._unsub_dispatcher.append( + async_dispatcher_connect( + self.hass, + service_signal(service, self.entity_id), + getattr(self, params[1]), + ) + ) + self._unsub_dispatcher.append( + async_dispatcher_connect( + self.hass, + service_signal(SERVICE_UPDATE, self._name), + self.async_on_demand_update, + ) + ) + self.hass.data[DATA_AMCREST][CAMERAS].append(self.entity_id) + + async def async_will_remove_from_hass(self): + """Remove camera from list and disconnect from signals.""" + self.hass.data[DATA_AMCREST][CAMERAS].remove(self.entity_id) + for unsub_dispatcher in self._unsub_dispatcher: + unsub_dispatcher() + + def update(self): + """Update entity status.""" + if not self.available or self._update_succeeded: + if not self.available: + self._update_succeeded = False + return + _LOGGER.debug("Updating %s camera", self.name) + try: + if self._brand is None: + resp = self._api.vendor_information.strip() + if resp.startswith("vendor="): + self._brand = resp.split("=")[-1] + else: + self._brand = "unknown" + if self._model is None: + resp = self._api.device_type.strip() + if resp.startswith("type="): + self._model = resp.split("=")[-1] + else: + self._model = "unknown" + self.is_streaming = self._get_video() + self._is_recording = self._get_recording() + self._motion_detection_enabled = self._get_motion_detection() + self._audio_enabled = self._get_audio() + self._motion_recording_enabled = self._get_motion_recording() + self._color_bw = self._get_color_mode() + self._rtsp_url = self._api.rtsp_url(typeno=self._resolution) + except AmcrestError as error: + log_update_error(_LOGGER, "get", self.name, "camera attributes", error) + self._update_succeeded = False + else: + self._update_succeeded = True + + # Other Camera method overrides + + def turn_off(self): + """Turn off camera.""" + self._enable_video(False) + + def turn_on(self): + """Turn on camera.""" + self._enable_video(True) + + def enable_motion_detection(self): + """Enable motion detection in the camera.""" + self._enable_motion_detection(True) + + def disable_motion_detection(self): + """Disable motion detection in camera.""" + self._enable_motion_detection(False) + + # Additional Amcrest Camera service methods + + async def async_enable_recording(self): + """Call the job and enable recording.""" + await self.hass.async_add_executor_job(self._enable_recording, True) + + async def async_disable_recording(self): + """Call the job and disable recording.""" + await self.hass.async_add_executor_job(self._enable_recording, False) + + async def async_enable_audio(self): + """Call the job and enable audio.""" + await self.hass.async_add_executor_job(self._enable_audio, True) + + async def async_disable_audio(self): + """Call the job and disable audio.""" + await self.hass.async_add_executor_job(self._enable_audio, False) + + async def async_enable_motion_recording(self): + """Call the job and enable motion recording.""" + await self.hass.async_add_executor_job(self._enable_motion_recording, True) + + async def async_disable_motion_recording(self): + """Call the job and disable motion recording.""" + await self.hass.async_add_executor_job(self._enable_motion_recording, False) + + async def async_goto_preset(self, preset): + """Call the job and move camera to preset position.""" + await self.hass.async_add_executor_job(self._goto_preset, preset) + + async def async_set_color_bw(self, color_bw): + """Call the job and set camera color mode.""" + await self.hass.async_add_executor_job(self._set_color_bw, color_bw) + + async def async_start_tour(self): + """Call the job and start camera tour.""" + await self.hass.async_add_executor_job(self._start_tour, True) + + async def async_stop_tour(self): + """Call the job and stop camera tour.""" + await self.hass.async_add_executor_job(self._start_tour, False) + + async def async_ptz_control(self, movement, travel_time): + """Move or zoom camera in specified direction.""" + code = _ACTION[_MOV.index(movement)] + + kwargs = {"code": code, "arg1": 0, "arg2": 0, "arg3": 0} + if code in _MOVE_1_ACTIONS: + kwargs["arg2"] = 1 + elif code in _MOVE_2_ACTIONS: + kwargs["arg1"] = kwargs["arg2"] = 1 + + try: + await self.hass.async_add_executor_job( + partial(self._api.ptz_control_command, action="start", **kwargs) + ) + await asyncio.sleep(travel_time) + await self.hass.async_add_executor_job( + partial(self._api.ptz_control_command, action="stop", **kwargs) + ) + except AmcrestError as error: + log_update_error( + _LOGGER, "move", self.name, f"camera PTZ {movement}", error + ) + + # Methods to send commands to Amcrest camera and handle errors + + def _change_setting(self, value, attr, description, action="set"): + func = description.replace(" ", "_") + description = f"camera {description} to {value}" + tries = 3 + while True: + try: + getattr(self, f"_set_{func}")(value) + new_value = getattr(self, f"_get_{func}")() + if new_value != value: + raise AmcrestCommandFailed + except (AmcrestError, AmcrestCommandFailed) as error: + if tries == 1: + log_update_error(_LOGGER, action, self.name, description, error) + return + log_update_error( + _LOGGER, action, self.name, description, error, logging.DEBUG + ) + else: + if attr: + setattr(self, attr, new_value) + self.schedule_update_ha_state() + return + tries -= 1 + + def _get_video(self): + return self._api.video_enabled + + def _set_video(self, enable): + self._api.video_enabled = enable + + def _enable_video(self, enable): + """Enable or disable camera video stream.""" + # Given the way the camera's state is determined by + # is_streaming and is_recording, we can't leave + # recording on if video stream is being turned off. + if self.is_recording and not enable: + self._enable_recording(False) + self._change_setting(enable, "is_streaming", "video") + if self._control_light: + self._change_light() + + def _get_recording(self): + return self._api.record_mode == "Manual" + + def _set_recording(self, enable): + rec_mode = {"Automatic": 0, "Manual": 1} + self._api.record_mode = rec_mode["Manual" if enable else "Automatic"] + + def _enable_recording(self, enable): + """Turn recording on or off.""" + # Given the way the camera's state is determined by + # is_streaming and is_recording, we can't leave + # video stream off if recording is being turned on. + if not self.is_streaming and enable: + self._enable_video(True) + self._change_setting(enable, "_is_recording", "recording") + + def _get_motion_detection(self): + return self._api.is_motion_detector_on() + + def _set_motion_detection(self, enable): + self._api.motion_detection = str(enable).lower() + + def _enable_motion_detection(self, enable): + """Enable or disable motion detection.""" + self._change_setting(enable, "_motion_detection_enabled", "motion detection") + + def _get_audio(self): + return self._api.audio_enabled + + def _set_audio(self, enable): + self._api.audio_enabled = enable + + def _enable_audio(self, enable): + """Enable or disable audio stream.""" + self._change_setting(enable, "_audio_enabled", "audio") + if self._control_light: + self._change_light() + + def _get_indicator_light(self): + return "true" in self._api.command( + "configManager.cgi?action=getConfig&name=LightGlobal" + ).content.decode("utf-8") + + def _set_indicator_light(self, enable): + self._api.command( + f"configManager.cgi?action=setConfig&LightGlobal[0].Enable={str(enable).lower()}" + ) + + def _change_light(self): + """Enable or disable indicator light.""" + self._change_setting( + self._audio_enabled or self.is_streaming, None, "indicator light" + ) + + def _get_motion_recording(self): + return self._api.is_record_on_motion_detection() + + def _set_motion_recording(self, enable): + self._api.motion_recording = str(enable).lower() + + def _enable_motion_recording(self, enable): + """Enable or disable motion recording.""" + self._change_setting(enable, "_motion_recording_enabled", "motion recording") + + def _goto_preset(self, preset): + """Move camera position and zoom to preset.""" + try: + self._api.go_to_preset(action="start", preset_point_number=preset) + except AmcrestError as error: + log_update_error( + _LOGGER, "move", self.name, f"camera to preset {preset}", error + ) + + def _get_color_mode(self): + return _CBW[self._api.day_night_color] + + def _set_color_mode(self, cbw): + self._api.day_night_color = _CBW.index(cbw) + + def _set_color_bw(self, cbw): + """Set camera color mode.""" + self._change_setting(cbw, "_color_bw", "color mode") + + def _start_tour(self, start): + """Start camera tour.""" + try: + self._api.tour(start=start) + except AmcrestError as error: + log_update_error( + _LOGGER, "start" if start else "stop", self.name, "camera tour", error + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/const.py new file mode 100644 index 00000000000..ba7597d61af --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/const.py @@ -0,0 +1,19 @@ +"""Constants for amcrest component.""" +DOMAIN = "amcrest" +DATA_AMCREST = DOMAIN +CAMERAS = "cameras" +DEVICES = "devices" + +BINARY_SENSOR_SCAN_INTERVAL_SECS = 5 +CAMERA_WEB_SESSION_TIMEOUT = 10 +COMM_RETRIES = 1 +COMM_TIMEOUT = 6.05 +SENSOR_SCAN_INTERVAL_SECS = 10 +SNAPSHOT_TIMEOUT = 20 + +SERVICE_EVENT = "event" +SERVICE_UPDATE = "update" + +SENSOR_DEVICE_CLASS = "class" +SENSOR_EVENT_CODE = "code" +SENSOR_NAME = "name" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/helpers.py b/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/helpers.py new file mode 100644 index 00000000000..ef0ae2db15b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/helpers.py @@ -0,0 +1,21 @@ +"""Helpers for amcrest component.""" +import logging + +from .const import DOMAIN + + +def service_signal(service, *args): + """Encode signal.""" + return "_".join([DOMAIN, service, *args]) + + +def log_update_error(logger, action, name, entity_type, error, level=logging.ERROR): + """Log an update error.""" + logger.log( + level, + "Could not %s %s %s due to error: %s", + action, + name, + entity_type, + error.__class__.__name__, + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/manifest.json new file mode 100644 index 00000000000..702e6a61487 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "amcrest", + "name": "Amcrest", + "documentation": "https://www.home-assistant.io/integrations/amcrest", + "requirements": ["amcrest==1.7.2"], + "dependencies": ["ffmpeg"], + "codeowners": [], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/sensor.py new file mode 100644 index 00000000000..a30de62494e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/sensor.py @@ -0,0 +1,135 @@ +"""Support for Amcrest IP camera sensors.""" +from datetime import timedelta +import logging + +from amcrest import AmcrestError + +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import CONF_NAME, CONF_SENSORS, PERCENTAGE +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_AMCREST, DEVICES, SENSOR_SCAN_INTERVAL_SECS, SERVICE_UPDATE +from .helpers import log_update_error, service_signal + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=SENSOR_SCAN_INTERVAL_SECS) + +SENSOR_PTZ_PRESET = "ptz_preset" +SENSOR_SDCARD = "sdcard" +# Sensor types are defined like: Name, units, icon +SENSORS = { + SENSOR_PTZ_PRESET: ["PTZ Preset", None, "mdi:camera-iris"], + SENSOR_SDCARD: ["SD Used", PERCENTAGE, "mdi:sd"], +} + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up a sensor for an Amcrest IP Camera.""" + if discovery_info is None: + return + + name = discovery_info[CONF_NAME] + device = hass.data[DATA_AMCREST][DEVICES][name] + async_add_entities( + [ + AmcrestSensor(name, device, sensor_type) + for sensor_type in discovery_info[CONF_SENSORS] + ], + True, + ) + + +class AmcrestSensor(SensorEntity): + """A sensor implementation for Amcrest IP camera.""" + + def __init__(self, name, device, sensor_type): + """Initialize a sensor for Amcrest camera.""" + self._name = f"{name} {SENSORS[sensor_type][0]}" + self._signal_name = name + self._api = device.api + self._sensor_type = sensor_type + self._state = None + self._attrs = {} + self._unit_of_measurement = SENSORS[sensor_type][1] + self._icon = SENSORS[sensor_type][2] + self._unsub_dispatcher = None + + @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._state + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return self._attrs + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def unit_of_measurement(self): + """Return the units of measurement.""" + return self._unit_of_measurement + + @property + def available(self): + """Return True if entity is available.""" + return self._api.available + + def update(self): + """Get the latest data and updates the state.""" + if not self.available: + return + _LOGGER.debug("Updating %s sensor", self._name) + + try: + if self._sensor_type == SENSOR_PTZ_PRESET: + self._state = self._api.ptz_presets_count + + elif self._sensor_type == SENSOR_SDCARD: + storage = self._api.storage_all + try: + self._attrs[ + "Total" + ] = f"{storage['total'][0]:.2f} {storage['total'][1]}" + except ValueError: + self._attrs[ + "Total" + ] = f"{storage['total'][0]} {storage['total'][1]}" + try: + self._attrs[ + "Used" + ] = f"{storage['used'][0]:.2f} {storage['used'][1]}" + except ValueError: + self._attrs["Used"] = f"{storage['used'][0]} {storage['used'][1]}" + try: + self._state = f"{storage['used_percent']:.2f}" + except ValueError: + self._state = storage["used_percent"] + except AmcrestError as error: + log_update_error(_LOGGER, "update", self.name, "sensor", error) + + async def async_on_demand_update(self): + """Update state.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Subscribe to update signal.""" + self._unsub_dispatcher = async_dispatcher_connect( + self.hass, + service_signal(SERVICE_UPDATE, self._signal_name), + self.async_on_demand_update, + ) + + async def async_will_remove_from_hass(self): + """Disconnect from update signal.""" + self._unsub_dispatcher() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/services.yaml b/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/services.yaml new file mode 100644 index 00000000000..c4a12c59828 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/amcrest/services.yaml @@ -0,0 +1,165 @@ +enable_recording: + name: Enable recording + description: Enable continuous recording to camera storage. + fields: + entity_id: + name: Entity + description: "Name(s) of the cameras, or 'all' for all cameras." + example: "camera.house_front" + selector: + text: + +disable_recording: + name: Disable recording + description: Disable continuous recording to camera storage. + fields: + entity_id: + name: Entity + description: "Name(s) of the cameras, or 'all' for all cameras." + example: "camera.house_front" + selector: + text: + +enable_audio: + name: Enable audio + description: Enable audio stream. + fields: + entity_id: + name: Entity + description: "Name(s) of the cameras, or 'all' for all cameras." + example: "camera.house_front" + selector: + text: + +disable_audio: + name: Disable audio + description: Disable audio stream. + fields: + entity_id: + name: Entity + description: "Name(s) of the cameras, or 'all' for all cameras." + example: "camera.house_front" + selector: + text: + +enable_motion_recording: + name: Enable motion recording + description: Enable recording a clip to camera storage when motion is detected. + fields: + entity_id: + name: Entity + description: "Name(s) of the cameras, or 'all' for all cameras." + example: "camera.house_front" + selector: + text: + +disable_motion_recording: + name: Disable motion recording + description: Disable recording a clip to camera storage when motion is detected. + fields: + entity_id: + name: Entity + description: "Name(s) of the cameras, or 'all' for all cameras." + example: "camera.house_front" + selector: + text: + +goto_preset: + name: Go to preset + description: Move camera to PTZ preset. + fields: + entity_id: + description: "Name(s) of the cameras, or 'all' for all cameras." + example: "camera.house_front" + preset: + name: Preset + description: Preset number. + required: true + example: 1 + selector: + number: + min: 1 + max: 1000 + +set_color_bw: + name: Set color + description: Set camera color mode. + fields: + entity_id: + name: Entity + description: "Name(s) of the cameras, or 'all' for all cameras." + example: "camera.house_front" + selector: + text: + color_bw: + name: Color + description: Color mode. + example: auto + selector: + select: + options: + - 'auto' + - 'bw' + - 'color' + +start_tour: + name: Start tour + description: Start camera's PTZ tour function. + fields: + entity_id: + name: Entity + description: "Name(s) of the cameras, or 'all' for all cameras." + example: "camera.house_front" + selector: + text: + +stop_tour: + name: Stop tour + description: Stop camera's PTZ tour function. + fields: + entity_id: + name: Entity + description: "Name(s) of the cameras, or 'all' for all cameras." + example: "camera.house_front" + selector: + text: + +ptz_control: + name: PTZ control + description: Move (Pan/Tilt) and/or Zoom a PTZ camera. + fields: + entity_id: + name: Entity + description: "Name of the camera, or 'all' for all cameras." + example: "camera.house_front" + selector: + text: + movement: + name: Movement + description: "Direction to move the camera." + required: true + example: "right" + selector: + select: + options: + - 'down' + - 'left' + - 'left_down' + - 'left_up' + - 'right' + - 'right_down' + - 'right_up' + - 'up' + - 'zoom_in' + - 'zoom_out' + travel_time: + name: Travel time + description: "Travel time in fractional seconds: from 0 to 1." + example: ".5" + default: .2 + selector: + number: + min: 0 + max: 1 + step: 0.01 + unit_of_measurement: seconds diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ampio/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/ampio/__init__.py new file mode 100644 index 00000000000..5f7bb4a44fa --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ampio/__init__.py @@ -0,0 +1 @@ +"""The Ampio component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ampio/air_quality.py b/homeassistant-2021.6.0.dev0/homeassistant/components/ampio/air_quality.py new file mode 100644 index 00000000000..f8119e9c1b4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ampio/air_quality.py @@ -0,0 +1,105 @@ +"""Support for Ampio Air Quality data.""" +from __future__ import annotations + +import logging +from typing import Final + +from asmog import AmpioSmog +import voluptuous as vol + +from homeassistant.components.air_quality import ( + PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, + AirQualityEntity, +) +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util import Throttle + +from .const import ATTRIBUTION, CONF_STATION_ID, SCAN_INTERVAL + +_LOGGER: Final = logging.getLogger(__name__) + +PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( + {vol.Required(CONF_STATION_ID): cv.string, vol.Optional(CONF_NAME): cv.string} +) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Ampio Smog air quality platform.""" + + name = config.get(CONF_NAME) + station_id = config[CONF_STATION_ID] + + session = async_get_clientsession(hass) + api = AmpioSmogMapData(AmpioSmog(station_id, hass.loop, session)) + + await api.async_update() + + if not api.api.data: + _LOGGER.error("Station %s is not available", station_id) + return + + async_add_entities([AmpioSmogQuality(api, station_id, name)], True) + + +class AmpioSmogQuality(AirQualityEntity): + """Implementation of an Ampio Smog air quality entity.""" + + def __init__( + self, api: AmpioSmogMapData, station_id: str, name: str | None + ) -> None: + """Initialize the air quality entity.""" + self._ampio = api + self._station_id = station_id + self._name = name or api.api.name + + @property + def name(self) -> str: + """Return the name of the air quality entity.""" + return self._name + + @property + def unique_id(self) -> str: + """Return unique_name.""" + return f"ampio_smog_{self._station_id}" + + @property + def particulate_matter_2_5(self) -> str | None: + """Return the particulate matter 2.5 level.""" + return self._ampio.api.pm2_5 # type: ignore[no-any-return] + + @property + def particulate_matter_10(self) -> str | None: + """Return the particulate matter 10 level.""" + return self._ampio.api.pm10 # type: ignore[no-any-return] + + @property + def attribution(self) -> str: + """Return the attribution.""" + return ATTRIBUTION + + async def async_update(self) -> None: + """Get the latest data from the AmpioMap API.""" + await self._ampio.async_update() + + +class AmpioSmogMapData: + """Get the latest data and update the states.""" + + def __init__(self, api: AmpioSmog) -> None: + """Initialize the data object.""" + self.api = api + + @Throttle(SCAN_INTERVAL) + async def async_update(self) -> None: + """Get the latest data from AmpioMap.""" + await self.api.get_data() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ampio/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/ampio/const.py new file mode 100644 index 00000000000..3162308ff41 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ampio/const.py @@ -0,0 +1,7 @@ +"""Constants for Ampio Air Quality platform.""" +from datetime import timedelta +from typing import Final + +ATTRIBUTION: Final = "Data provided by Ampio" +CONF_STATION_ID: Final = "station_id" +SCAN_INTERVAL: Final = timedelta(minutes=10) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/ampio/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/ampio/manifest.json new file mode 100644 index 00000000000..b47f84f2fe5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/ampio/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "ampio", + "name": "Ampio Smart Smog System", + "documentation": "https://www.home-assistant.io/integrations/ampio", + "requirements": ["asmog==0.0.6"], + "codeowners": [], + "iot_class": "cloud_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/analytics/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/analytics/__init__.py new file mode 100644 index 00000000000..d41970a79de --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/analytics/__init__.py @@ -0,0 +1,76 @@ +"""Send instance and usage analytics.""" +import voluptuous as vol + +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 .analytics import Analytics +from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA + + +async def async_setup(hass: HomeAssistant, _): + """Set up the analytics integration.""" + analytics = Analytics(hass) + + # Load stored data + await analytics.load() + + async def start_schedule(_event): + """Start the send schedule after the started event.""" + # Wait 15 min after started + async_call_later(hass, 900, analytics.send_analytics) + + # Send every day + async_track_time_interval(hass, analytics.send_analytics, INTERVAL) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_schedule) + + websocket_api.async_register_command(hass, websocket_analytics) + websocket_api.async_register_command(hass, websocket_analytics_preferences) + + hass.data[DOMAIN] = analytics + return True + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command({vol.Required("type"): "analytics"}) +async def websocket_analytics( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Return analytics preferences.""" + analytics: Analytics = hass.data[DOMAIN] + connection.send_result( + msg["id"], + {ATTR_PREFERENCES: analytics.preferences, ATTR_ONBOARDED: analytics.onboarded}, + ) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "analytics/preferences", + vol.Required("preferences", default={}): PREFERENCE_SCHEMA, + } +) +async def websocket_analytics_preferences( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Update analytics preferences.""" + preferences = msg[ATTR_PREFERENCES] + analytics: Analytics = hass.data[DOMAIN] + + await analytics.save_preferences(preferences) + await analytics.send_analytics() + + connection.send_result( + msg["id"], + {ATTR_PREFERENCES: analytics.preferences}, + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/analytics/analytics.py b/homeassistant-2021.6.0.dev0/homeassistant/components/analytics/analytics.py new file mode 100644 index 00000000000..e6e7ffac337 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/analytics/analytics.py @@ -0,0 +1,261 @@ +"""Analytics helper class for the analytics integration.""" +import asyncio +import uuid + +import aiohttp +import async_timeout + +from homeassistant.components import hassio +from homeassistant.components.api import ATTR_INSTALLATION_TYPE +from homeassistant.components.automation.const import DOMAIN as AUTOMATION_DOMAIN +from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.storage import Store +from homeassistant.helpers.system_info import async_get_system_info +from homeassistant.loader import IntegrationNotFound, async_get_integration +from homeassistant.setup import async_get_loaded_integrations + +from .const import ( + ANALYTICS_ENDPOINT_URL, + ANALYTICS_ENDPOINT_URL_DEV, + ATTR_ADDON_COUNT, + ATTR_ADDONS, + ATTR_AUTO_UPDATE, + ATTR_AUTOMATION_COUNT, + ATTR_BASE, + ATTR_BOARD, + ATTR_CUSTOM_INTEGRATIONS, + ATTR_DIAGNOSTICS, + ATTR_HEALTHY, + ATTR_INTEGRATION_COUNT, + ATTR_INTEGRATIONS, + ATTR_ONBOARDED, + ATTR_OPERATING_SYSTEM, + ATTR_PREFERENCES, + ATTR_PROTECTED, + ATTR_SLUG, + ATTR_STATE_COUNT, + ATTR_STATISTICS, + ATTR_SUPERVISOR, + ATTR_SUPPORTED, + ATTR_USAGE, + ATTR_USER_COUNT, + ATTR_UUID, + ATTR_VERSION, + LOGGER, + PREFERENCE_SCHEMA, + STORAGE_KEY, + STORAGE_VERSION, +) + + +class Analytics: + """Analytics helper class for the analytics integration.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the Analytics class.""" + self.hass: HomeAssistant = hass + self.session = async_get_clientsession(hass) + self._data = {ATTR_PREFERENCES: {}, ATTR_ONBOARDED: False, ATTR_UUID: None} + self._store: Store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + + @property + def preferences(self) -> dict: + """Return the current active preferences.""" + preferences = self._data[ATTR_PREFERENCES] + return { + ATTR_BASE: preferences.get(ATTR_BASE, False), + ATTR_DIAGNOSTICS: preferences.get(ATTR_DIAGNOSTICS, False), + ATTR_USAGE: preferences.get(ATTR_USAGE, False), + ATTR_STATISTICS: preferences.get(ATTR_STATISTICS, False), + } + + @property + def onboarded(self) -> bool: + """Return bool if the user has made a choice.""" + return self._data[ATTR_ONBOARDED] + + @property + def uuid(self) -> bool: + """Return the uuid for the analytics integration.""" + return self._data[ATTR_UUID] + + @property + def endpoint(self) -> str: + """Return the endpoint that will receive the payload.""" + if HA_VERSION.endswith("0.dev0"): + # dev installations will contact the dev analytics environment + return ANALYTICS_ENDPOINT_URL_DEV + return ANALYTICS_ENDPOINT_URL + + @property + def supervisor(self) -> bool: + """Return bool if a supervisor is present.""" + return hassio.is_hassio(self.hass) + + async def load(self) -> None: + """Load preferences.""" + stored = await self._store.async_load() + if stored: + self._data = stored + + if self.supervisor: + supervisor_info = hassio.get_supervisor_info(self.hass) + if not self.onboarded: + # User have not configured analytics, get this setting from the supervisor + if supervisor_info[ATTR_DIAGNOSTICS] and not self.preferences.get( + ATTR_DIAGNOSTICS, False + ): + self._data[ATTR_PREFERENCES][ATTR_DIAGNOSTICS] = True + elif not supervisor_info[ATTR_DIAGNOSTICS] and self.preferences.get( + ATTR_DIAGNOSTICS, False + ): + self._data[ATTR_PREFERENCES][ATTR_DIAGNOSTICS] = False + + async def save_preferences(self, preferences: dict) -> None: + """Save preferences.""" + preferences = PREFERENCE_SCHEMA(preferences) + self._data[ATTR_PREFERENCES].update(preferences) + self._data[ATTR_ONBOARDED] = True + + await self._store.async_save(self._data) + + if self.supervisor: + await hassio.async_update_diagnostics( + self.hass, self.preferences.get(ATTR_DIAGNOSTICS, False) + ) + + async def send_analytics(self, _=None) -> None: + """Send analytics.""" + supervisor_info = None + operating_system_info = {} + + if not self.onboarded or not self.preferences.get(ATTR_BASE, False): + LOGGER.debug("Nothing to submit") + return + + if self._data.get(ATTR_UUID) is None: + self._data[ATTR_UUID] = uuid.uuid4().hex + await self._store.async_save(self._data) + + if self.supervisor: + supervisor_info = hassio.get_supervisor_info(self.hass) + operating_system_info = hassio.get_os_info(self.hass) + + system_info = await async_get_system_info(self.hass) + integrations = [] + custom_integrations = [] + addons = [] + payload: dict = { + ATTR_UUID: self.uuid, + ATTR_VERSION: HA_VERSION, + ATTR_INSTALLATION_TYPE: system_info[ATTR_INSTALLATION_TYPE], + } + + if supervisor_info is not None: + payload[ATTR_SUPERVISOR] = { + ATTR_HEALTHY: supervisor_info[ATTR_HEALTHY], + ATTR_SUPPORTED: supervisor_info[ATTR_SUPPORTED], + } + + if operating_system_info.get(ATTR_BOARD) is not None: + payload[ATTR_OPERATING_SYSTEM] = { + ATTR_BOARD: operating_system_info[ATTR_BOARD], + ATTR_VERSION: operating_system_info[ATTR_VERSION], + } + + if self.preferences.get(ATTR_USAGE, False) or self.preferences.get( + ATTR_STATISTICS, False + ): + configured_integrations = await asyncio.gather( + *[ + async_get_integration(self.hass, domain) + for domain in async_get_loaded_integrations(self.hass) + ], + return_exceptions=True, + ) + + for integration in configured_integrations: + if isinstance(integration, IntegrationNotFound): + continue + + if isinstance(integration, BaseException): + raise integration + + if integration.disabled: + continue + + if not integration.is_built_in: + custom_integrations.append( + { + ATTR_DOMAIN: integration.domain, + ATTR_VERSION: integration.version, + } + ) + continue + + integrations.append(integration.domain) + + if supervisor_info is not None: + installed_addons = await asyncio.gather( + *[ + hassio.async_get_addon_info(self.hass, addon[ATTR_SLUG]) + for addon in supervisor_info[ATTR_ADDONS] + ] + ) + for addon in installed_addons: + addons.append( + { + ATTR_SLUG: addon[ATTR_SLUG], + ATTR_PROTECTED: addon[ATTR_PROTECTED], + ATTR_VERSION: addon[ATTR_VERSION], + ATTR_AUTO_UPDATE: addon[ATTR_AUTO_UPDATE], + } + ) + + if self.preferences.get(ATTR_USAGE, False): + payload[ATTR_INTEGRATIONS] = integrations + payload[ATTR_CUSTOM_INTEGRATIONS] = custom_integrations + if supervisor_info is not None: + payload[ATTR_ADDONS] = addons + + if self.preferences.get(ATTR_STATISTICS, False): + payload[ATTR_STATE_COUNT] = len(self.hass.states.async_all()) + payload[ATTR_AUTOMATION_COUNT] = len( + self.hass.states.async_all(AUTOMATION_DOMAIN) + ) + payload[ATTR_INTEGRATION_COUNT] = len(integrations) + if supervisor_info is not None: + payload[ATTR_ADDON_COUNT] = len(addons) + payload[ATTR_USER_COUNT] = len( + [ + user + for user in await self.hass.auth.async_get_users() + if not user.system_generated + ] + ) + + try: + with async_timeout.timeout(30): + response = await self.session.post(self.endpoint, json=payload) + if response.status == 200: + LOGGER.info( + ( + "Submitted analytics to Home Assistant servers. " + "Information submitted includes %s" + ), + payload, + ) + else: + LOGGER.warning( + "Sending analytics failed with statuscode %s from %s", + response.status, + self.endpoint, + ) + except asyncio.TimeoutError: + LOGGER.error("Timeout sending analytics to %s", ANALYTICS_ENDPOINT_URL) + except aiohttp.ClientError as err: + LOGGER.error( + "Error sending analytics to %s: %r", ANALYTICS_ENDPOINT_URL, err + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/analytics/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/analytics/const.py new file mode 100644 index 00000000000..16929a7131d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/analytics/const.py @@ -0,0 +1,51 @@ +"""Constants for the analytics integration.""" +from datetime import timedelta +import logging + +import voluptuous as vol + +ANALYTICS_ENDPOINT_URL = "https://analytics-api.home-assistant.io/v1" +ANALYTICS_ENDPOINT_URL_DEV = "https://analytics-api-dev.home-assistant.io/v1" +DOMAIN = "analytics" +INTERVAL = timedelta(days=1) +STORAGE_KEY = "core.analytics" +STORAGE_VERSION = 1 + + +LOGGER: logging.Logger = logging.getLogger(__package__) + +ATTR_ADDON_COUNT = "addon_count" +ATTR_ADDONS = "addons" +ATTR_AUTO_UPDATE = "auto_update" +ATTR_AUTOMATION_COUNT = "automation_count" +ATTR_BASE = "base" +ATTR_BOARD = "board" +ATTR_CUSTOM_INTEGRATIONS = "custom_integrations" +ATTR_DIAGNOSTICS = "diagnostics" +ATTR_HEALTHY = "healthy" +ATTR_INSTALLATION_TYPE = "installation_type" +ATTR_INTEGRATION_COUNT = "integration_count" +ATTR_INTEGRATIONS = "integrations" +ATTR_ONBOARDED = "onboarded" +ATTR_OPERATING_SYSTEM = "operating_system" +ATTR_PREFERENCES = "preferences" +ATTR_PROTECTED = "protected" +ATTR_SLUG = "slug" +ATTR_STATE_COUNT = "state_count" +ATTR_STATISTICS = "statistics" +ATTR_SUPERVISOR = "supervisor" +ATTR_SUPPORTED = "supported" +ATTR_USAGE = "usage" +ATTR_USER_COUNT = "user_count" +ATTR_UUID = "uuid" +ATTR_VERSION = "version" + + +PREFERENCE_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_BASE): bool, + vol.Optional(ATTR_DIAGNOSTICS): bool, + vol.Optional(ATTR_STATISTICS): bool, + vol.Optional(ATTR_USAGE): bool, + } +) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/analytics/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/analytics/manifest.json new file mode 100644 index 00000000000..49edf1bcf8c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/analytics/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "analytics", + "name": "Analytics", + "documentation": "https://www.home-assistant.io/integrations/analytics", + "codeowners": ["@home-assistant/core", "@ludeeus"], + "dependencies": ["api", "websocket_api"], + "quality_scale": "internal", + "iot_class": "cloud_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/android_ip_webcam/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/android_ip_webcam/__init__.py new file mode 100644 index 00000000000..54a281feacd --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/android_ip_webcam/__init__.py @@ -0,0 +1,333 @@ +"""Support for Android IP Webcam.""" +import asyncio +from datetime import timedelta + +from pydroid_ipcam import PyDroidIPCam +import voluptuous as vol + +from homeassistant.components.mjpeg.camera import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PLATFORM, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_SENSORS, + CONF_SWITCHES, + CONF_TIMEOUT, + CONF_USERNAME, +) +from homeassistant.core import callback +from homeassistant.helpers import discovery +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.util.dt import utcnow + +ATTR_AUD_CONNS = "Audio Connections" +ATTR_HOST = "host" +ATTR_VID_CONNS = "Video Connections" + +CONF_MOTION_SENSOR = "motion_sensor" + +DATA_IP_WEBCAM = "android_ip_webcam" +DEFAULT_NAME = "IP Webcam" +DEFAULT_PORT = 8080 +DEFAULT_TIMEOUT = 10 +DOMAIN = "android_ip_webcam" + +SCAN_INTERVAL = timedelta(seconds=10) +SIGNAL_UPDATE_DATA = "android_ip_webcam_update" + +KEY_MAP = { + "audio_connections": "Audio Connections", + "adet_limit": "Audio Trigger Limit", + "antibanding": "Anti-banding", + "audio_only": "Audio Only", + "battery_level": "Battery Level", + "battery_temp": "Battery Temperature", + "battery_voltage": "Battery Voltage", + "coloreffect": "Color Effect", + "exposure": "Exposure Level", + "exposure_lock": "Exposure Lock", + "ffc": "Front-facing Camera", + "flashmode": "Flash Mode", + "focus": "Focus", + "focus_homing": "Focus Homing", + "focus_region": "Focus Region", + "focusmode": "Focus Mode", + "gps_active": "GPS Active", + "idle": "Idle", + "ip_address": "IPv4 Address", + "ipv6_address": "IPv6 Address", + "ivideon_streaming": "Ivideon Streaming", + "light": "Light Level", + "mirror_flip": "Mirror Flip", + "motion": "Motion", + "motion_active": "Motion Active", + "motion_detect": "Motion Detection", + "motion_event": "Motion Event", + "motion_limit": "Motion Limit", + "night_vision": "Night Vision", + "night_vision_average": "Night Vision Average", + "night_vision_gain": "Night Vision Gain", + "orientation": "Orientation", + "overlay": "Overlay", + "photo_size": "Photo Size", + "pressure": "Pressure", + "proximity": "Proximity", + "quality": "Quality", + "scenemode": "Scene Mode", + "sound": "Sound", + "sound_event": "Sound Event", + "sound_timeout": "Sound Timeout", + "torch": "Torch", + "video_connections": "Video Connections", + "video_chunk_len": "Video Chunk Length", + "video_recording": "Video Recording", + "video_size": "Video Size", + "whitebalance": "White Balance", + "whitebalance_lock": "White Balance Lock", + "zoom": "Zoom", +} + +ICON_MAP = { + "audio_connections": "mdi:speaker", + "battery_level": "mdi:battery", + "battery_temp": "mdi:thermometer", + "battery_voltage": "mdi:battery-charging-100", + "exposure_lock": "mdi:camera", + "ffc": "mdi:camera-front-variant", + "focus": "mdi:image-filter-center-focus", + "gps_active": "mdi:crosshairs-gps", + "light": "mdi:flashlight", + "motion": "mdi:run", + "night_vision": "mdi:weather-night", + "overlay": "mdi:monitor", + "pressure": "mdi:gauge", + "proximity": "mdi:map-marker-radius", + "quality": "mdi:quality-high", + "sound": "mdi:speaker", + "sound_event": "mdi:speaker", + "sound_timeout": "mdi:speaker", + "torch": "mdi:white-balance-sunny", + "video_chunk_len": "mdi:video", + "video_connections": "mdi:eye", + "video_recording": "mdi:record-rec", + "whitebalance_lock": "mdi:white-balance-auto", +} + +SWITCHES = [ + "exposure_lock", + "ffc", + "focus", + "gps_active", + "motion_detect", + "night_vision", + "overlay", + "torch", + "whitebalance_lock", + "video_recording", +] + +SENSORS = [ + "audio_connections", + "battery_level", + "battery_temp", + "battery_voltage", + "light", + "motion", + "pressure", + "proximity", + "sound", + "video_connections", +] + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional( + CONF_TIMEOUT, default=DEFAULT_TIMEOUT + ): cv.positive_int, + vol.Optional( + CONF_SCAN_INTERVAL, default=SCAN_INTERVAL + ): cv.time_period, + vol.Inclusive(CONF_USERNAME, "authentication"): cv.string, + vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string, + vol.Optional(CONF_SWITCHES): vol.All( + cv.ensure_list, [vol.In(SWITCHES)] + ), + vol.Optional(CONF_SENSORS): vol.All( + cv.ensure_list, [vol.In(SENSORS)] + ), + vol.Optional(CONF_MOTION_SENSOR): cv.boolean, + } + ) + ], + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the IP Webcam component.""" + + webcams = hass.data[DATA_IP_WEBCAM] = {} + websession = async_get_clientsession(hass) + + async def async_setup_ipcamera(cam_config): + """Set up an IP camera.""" + host = cam_config[CONF_HOST] + username = cam_config.get(CONF_USERNAME) + password = cam_config.get(CONF_PASSWORD) + name = cam_config[CONF_NAME] + interval = cam_config[CONF_SCAN_INTERVAL] + switches = cam_config.get(CONF_SWITCHES) + sensors = cam_config.get(CONF_SENSORS) + motion = cam_config.get(CONF_MOTION_SENSOR) + + # Init ip webcam + cam = PyDroidIPCam( + hass.loop, + websession, + host, + cam_config[CONF_PORT], + username=username, + password=password, + timeout=cam_config[CONF_TIMEOUT], + ) + + if switches is None: + switches = [ + setting for setting in cam.enabled_settings if setting in SWITCHES + ] + + if sensors is None: + sensors = [sensor for sensor in cam.enabled_sensors if sensor in SENSORS] + sensors.extend(["audio_connections", "video_connections"]) + + if motion is None: + motion = "motion_active" in cam.enabled_sensors + + async def async_update_data(now): + """Update data from IP camera in SCAN_INTERVAL.""" + await cam.update() + async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host) + + async_track_point_in_utc_time(hass, async_update_data, utcnow() + interval) + + await async_update_data(None) + + # Load platforms + webcams[host] = cam + + mjpeg_camera = { + CONF_PLATFORM: "mjpeg", + CONF_MJPEG_URL: cam.mjpeg_url, + CONF_STILL_IMAGE_URL: cam.image_url, + CONF_NAME: name, + } + if username and password: + mjpeg_camera.update({CONF_USERNAME: username, CONF_PASSWORD: password}) + + hass.async_create_task( + discovery.async_load_platform(hass, "camera", "mjpeg", mjpeg_camera, config) + ) + + if sensors: + hass.async_create_task( + discovery.async_load_platform( + hass, + "sensor", + DOMAIN, + {CONF_NAME: name, CONF_HOST: host, CONF_SENSORS: sensors}, + config, + ) + ) + + if switches: + hass.async_create_task( + discovery.async_load_platform( + hass, + "switch", + DOMAIN, + {CONF_NAME: name, CONF_HOST: host, CONF_SWITCHES: switches}, + config, + ) + ) + + if motion: + hass.async_create_task( + discovery.async_load_platform( + hass, + "binary_sensor", + DOMAIN, + {CONF_HOST: host, CONF_NAME: name}, + config, + ) + ) + + tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]] + if tasks: + await asyncio.wait(tasks) + + return True + + +class AndroidIPCamEntity(Entity): + """The Android device running IP Webcam.""" + + def __init__(self, host, ipcam): + """Initialize the data object.""" + self._host = host + self._ipcam = ipcam + + async def async_added_to_hass(self): + """Register update dispatcher.""" + + @callback + def async_ipcam_update(host): + """Update callback.""" + if self._host != host: + return + self.async_schedule_update_ha_state(True) + + self.async_on_remove( + async_dispatcher_connect(self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update) + ) + + @property + def should_poll(self): + """Return True if entity has to be polled for state.""" + return False + + @property + def available(self): + """Return True if entity is available.""" + return self._ipcam.available + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + state_attr = {ATTR_HOST: self._host} + if self._ipcam.status_data is None: + return state_attr + + state_attr[ATTR_VID_CONNS] = self._ipcam.status_data.get("video_connections") + state_attr[ATTR_AUD_CONNS] = self._ipcam.status_data.get("audio_connections") + + return state_attr diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/android_ip_webcam/binary_sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/android_ip_webcam/binary_sensor.py new file mode 100644 index 00000000000..377ecfec667 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/android_ip_webcam/binary_sensor.py @@ -0,0 +1,53 @@ +"""Support for Android IP Webcam binary sensors.""" +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, + BinarySensorEntity, +) + +from . import CONF_HOST, CONF_NAME, DATA_IP_WEBCAM, KEY_MAP, AndroidIPCamEntity + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the IP Webcam binary sensors.""" + if discovery_info is None: + return + + host = discovery_info[CONF_HOST] + name = discovery_info[CONF_NAME] + ipcam = hass.data[DATA_IP_WEBCAM][host] + + async_add_entities([IPWebcamBinarySensor(name, host, ipcam, "motion_active")], True) + + +class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorEntity): + """Representation of an IP Webcam binary sensor.""" + + def __init__(self, name, host, ipcam, sensor): + """Initialize the binary sensor.""" + super().__init__(host, ipcam) + + self._sensor = sensor + self._mapped_name = KEY_MAP.get(self._sensor, self._sensor) + self._name = f"{name} {self._mapped_name}" + self._state = None + self._unit = None + + @property + def name(self): + """Return the name of the binary sensor, if any.""" + return self._name + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + async def async_update(self): + """Retrieve latest state.""" + state, _ = self._ipcam.export_sensor(self._sensor) + self._state = state == 1.0 + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_MOTION diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/android_ip_webcam/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/android_ip_webcam/manifest.json new file mode 100644 index 00000000000..637a773ac33 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/android_ip_webcam/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "android_ip_webcam", + "name": "Android IP Webcam", + "documentation": "https://www.home-assistant.io/integrations/android_ip_webcam", + "requirements": ["pydroid-ipcam==0.8"], + "codeowners": [], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/android_ip_webcam/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/android_ip_webcam/sensor.py new file mode 100644 index 00000000000..adedb297cd1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/android_ip_webcam/sensor.py @@ -0,0 +1,77 @@ +"""Support for Android IP Webcam sensors.""" +from homeassistant.components.sensor import SensorEntity +from homeassistant.helpers.icon import icon_for_battery_level + +from . import ( + CONF_HOST, + CONF_NAME, + CONF_SENSORS, + DATA_IP_WEBCAM, + ICON_MAP, + KEY_MAP, + AndroidIPCamEntity, +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the IP Webcam Sensor.""" + if discovery_info is None: + return + + host = discovery_info[CONF_HOST] + name = discovery_info[CONF_NAME] + sensors = discovery_info[CONF_SENSORS] + ipcam = hass.data[DATA_IP_WEBCAM][host] + + all_sensors = [] + + for sensor in sensors: + all_sensors.append(IPWebcamSensor(name, host, ipcam, sensor)) + + async_add_entities(all_sensors, True) + + +class IPWebcamSensor(AndroidIPCamEntity, SensorEntity): + """Representation of a IP Webcam sensor.""" + + def __init__(self, name, host, ipcam, sensor): + """Initialize the sensor.""" + super().__init__(host, ipcam) + + self._sensor = sensor + self._mapped_name = KEY_MAP.get(self._sensor, self._sensor) + self._name = f"{name} {self._mapped_name}" + self._state = None + self._unit = None + + @property + def name(self): + """Return the name of the sensor, if any.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + async def async_update(self): + """Retrieve latest state.""" + if self._sensor in ("audio_connections", "video_connections"): + if not self._ipcam.status_data: + return + self._state = self._ipcam.status_data.get(self._sensor) + self._unit = "Connections" + else: + self._state, self._unit = self._ipcam.export_sensor(self._sensor) + + @property + def icon(self): + """Return the icon for the sensor.""" + if self._sensor == "battery_level" and self._state is not None: + return icon_for_battery_level(int(self._state)) + return ICON_MAP.get(self._sensor, "mdi:eye") diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/android_ip_webcam/switch.py b/homeassistant-2021.6.0.dev0/homeassistant/components/android_ip_webcam/switch.py new file mode 100644 index 00000000000..bdbb37e7661 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/android_ip_webcam/switch.py @@ -0,0 +1,88 @@ +"""Support for Android IP Webcam settings.""" +from homeassistant.components.switch import SwitchEntity + +from . import ( + CONF_HOST, + CONF_NAME, + CONF_SWITCHES, + DATA_IP_WEBCAM, + ICON_MAP, + KEY_MAP, + AndroidIPCamEntity, +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the IP Webcam switch platform.""" + if discovery_info is None: + return + + host = discovery_info[CONF_HOST] + name = discovery_info[CONF_NAME] + switches = discovery_info[CONF_SWITCHES] + ipcam = hass.data[DATA_IP_WEBCAM][host] + + all_switches = [] + + for setting in switches: + all_switches.append(IPWebcamSettingsSwitch(name, host, ipcam, setting)) + + async_add_entities(all_switches, True) + + +class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchEntity): + """An abstract class for an IP Webcam setting.""" + + def __init__(self, name, host, ipcam, setting): + """Initialize the settings switch.""" + super().__init__(host, ipcam) + + self._setting = setting + self._mapped_name = KEY_MAP.get(self._setting, self._setting) + self._name = f"{name} {self._mapped_name}" + self._state = False + + @property + def name(self): + """Return the name of the node.""" + return self._name + + async def async_update(self): + """Get the updated status of the switch.""" + self._state = bool(self._ipcam.current_settings.get(self._setting)) + + @property + def is_on(self): + """Return the boolean response if the node is on.""" + return self._state + + async def async_turn_on(self, **kwargs): + """Turn device on.""" + if self._setting == "torch": + await self._ipcam.torch(activate=True) + elif self._setting == "focus": + await self._ipcam.focus(activate=True) + elif self._setting == "video_recording": + await self._ipcam.record(record=True) + else: + await self._ipcam.change_setting(self._setting, True) + self._state = True + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn device off.""" + if self._setting == "torch": + await self._ipcam.torch(activate=False) + elif self._setting == "focus": + await self._ipcam.focus(activate=False) + elif self._setting == "video_recording": + await self._ipcam.record(record=False) + else: + await self._ipcam.change_setting(self._setting, False) + self._state = False + self.async_write_ha_state() + + @property + def icon(self): + """Return the icon for the switch.""" + return ICON_MAP.get(self._setting, "mdi:flash") diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/androidtv/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/androidtv/__init__.py new file mode 100644 index 00000000000..14832aef315 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/androidtv/__init__.py @@ -0,0 +1 @@ +"""Support for functionality to interact with Android TV/Fire TV devices.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/androidtv/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/androidtv/manifest.json new file mode 100644 index 00000000000..9ab02fec68a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/androidtv/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "androidtv", + "name": "Android TV", + "documentation": "https://www.home-assistant.io/integrations/androidtv", + "requirements": [ + "adb-shell[async]==0.3.1", + "androidtv[async]==0.0.59", + "pure-python-adb[async]==0.3.0.dev0" + ], + "codeowners": ["@JeffLIrion"], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/androidtv/media_player.py b/homeassistant-2021.6.0.dev0/homeassistant/components/androidtv/media_player.py new file mode 100644 index 00000000000..5db73d14914 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/androidtv/media_player.py @@ -0,0 +1,788 @@ +"""Support for functionality to interact with Android TV / Fire TV devices.""" +from datetime import datetime +import functools +import logging +import os + +from adb_shell.auth.keygen import keygen +from adb_shell.exceptions import ( + AdbTimeoutError, + InvalidChecksumError, + InvalidCommandError, + InvalidResponseError, + TcpTimeoutException, +) +from androidtv import ha_state_detection_rules_validator +from androidtv.adb_manager.adb_manager_sync import ADBPythonSync +from androidtv.constants import APPS, KEYS +from androidtv.exceptions import LockNotAcquiredException +from androidtv.setup_async import setup +import voluptuous as vol + +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity +from homeassistant.components.media_player.const import ( + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, +) +from homeassistant.const import ( + ATTR_COMMAND, + ATTR_ENTITY_ID, + CONF_DEVICE_CLASS, + CONF_HOST, + CONF_NAME, + CONF_PORT, + EVENT_HOMEASSISTANT_STOP, + STATE_IDLE, + STATE_OFF, + STATE_PAUSED, + STATE_PLAYING, + STATE_STANDBY, +) +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.storage import STORAGE_DIR + +ANDROIDTV_DOMAIN = "androidtv" + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_ANDROIDTV = ( + SUPPORT_PAUSE + | SUPPORT_PLAY + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_SELECT_SOURCE + | SUPPORT_STOP + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_STEP +) + +SUPPORT_FIRETV = ( + SUPPORT_PAUSE + | SUPPORT_PLAY + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_SELECT_SOURCE + | SUPPORT_STOP +) + +ATTR_DEVICE_PATH = "device_path" +ATTR_LOCAL_PATH = "local_path" + +CONF_ADBKEY = "adbkey" +CONF_ADB_SERVER_IP = "adb_server_ip" +CONF_ADB_SERVER_PORT = "adb_server_port" +CONF_APPS = "apps" +CONF_EXCLUDE_UNNAMED_APPS = "exclude_unnamed_apps" +CONF_GET_SOURCES = "get_sources" +CONF_STATE_DETECTION_RULES = "state_detection_rules" +CONF_TURN_ON_COMMAND = "turn_on_command" +CONF_TURN_OFF_COMMAND = "turn_off_command" +CONF_SCREENCAP = "screencap" + +DEFAULT_NAME = "Android TV" +DEFAULT_PORT = 5555 +DEFAULT_ADB_SERVER_PORT = 5037 +DEFAULT_GET_SOURCES = True +DEFAULT_DEVICE_CLASS = "auto" +DEFAULT_SCREENCAP = True + +DEVICE_ANDROIDTV = "androidtv" +DEVICE_FIRETV = "firetv" +DEVICE_CLASSES = [DEFAULT_DEVICE_CLASS, DEVICE_ANDROIDTV, DEVICE_FIRETV] + +SERVICE_ADB_COMMAND = "adb_command" +SERVICE_DOWNLOAD = "download" +SERVICE_LEARN_SENDEVENT = "learn_sendevent" +SERVICE_UPLOAD = "upload" + +SERVICE_ADB_COMMAND_SCHEMA = vol.Schema( + {vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_COMMAND): cv.string} +) + +SERVICE_DOWNLOAD_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_DEVICE_PATH): cv.string, + vol.Required(ATTR_LOCAL_PATH): cv.string, + } +) + +SERVICE_UPLOAD_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_DEVICE_PATH): cv.string, + vol.Required(ATTR_LOCAL_PATH): cv.string, + } +) + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): vol.In( + DEVICE_CLASSES + ), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_ADBKEY): cv.isfile, + vol.Optional(CONF_ADB_SERVER_IP): cv.string, + vol.Optional(CONF_ADB_SERVER_PORT, default=DEFAULT_ADB_SERVER_PORT): cv.port, + vol.Optional(CONF_GET_SOURCES, default=DEFAULT_GET_SOURCES): cv.boolean, + vol.Optional(CONF_APPS, default={}): vol.Schema( + {cv.string: vol.Any(cv.string, None)} + ), + vol.Optional(CONF_TURN_ON_COMMAND): cv.string, + vol.Optional(CONF_TURN_OFF_COMMAND): cv.string, + vol.Optional(CONF_STATE_DETECTION_RULES, default={}): vol.Schema( + {cv.string: ha_state_detection_rules_validator(vol.Invalid)} + ), + vol.Optional(CONF_EXCLUDE_UNNAMED_APPS, default=False): cv.boolean, + vol.Optional(CONF_SCREENCAP, default=DEFAULT_SCREENCAP): cv.boolean, + } +) + +# Translate from `AndroidTV` / `FireTV` reported state to HA state. +ANDROIDTV_STATES = { + "off": STATE_OFF, + "idle": STATE_IDLE, + "standby": STATE_STANDBY, + "playing": STATE_PLAYING, + "paused": STATE_PAUSED, +} + + +def setup_androidtv(hass, config): + """Generate an ADB key (if needed) and load it.""" + adbkey = config.get(CONF_ADBKEY, hass.config.path(STORAGE_DIR, "androidtv_adbkey")) + if CONF_ADB_SERVER_IP not in config: + # Use "adb_shell" (Python ADB implementation) + if not os.path.isfile(adbkey): + # Generate ADB key files + keygen(adbkey) + + # Load the ADB key + signer = ADBPythonSync.load_adbkey(adbkey) + adb_log = f"using Python ADB implementation with adbkey='{adbkey}'" + + else: + # Use "pure-python-adb" (communicate with ADB server) + signer = None + adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}" + + return adbkey, signer, adb_log + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Android TV / Fire TV platform.""" + hass.data.setdefault(ANDROIDTV_DOMAIN, {}) + + address = f"{config[CONF_HOST]}:{config[CONF_PORT]}" + + if address in hass.data[ANDROIDTV_DOMAIN]: + _LOGGER.warning("Platform already setup on %s, skipping", address) + return + + adbkey, signer, adb_log = await hass.async_add_executor_job( + setup_androidtv, hass, config + ) + + aftv = await setup( + config[CONF_HOST], + config[CONF_PORT], + adbkey, + config.get(CONF_ADB_SERVER_IP, ""), + config[CONF_ADB_SERVER_PORT], + config[CONF_STATE_DETECTION_RULES], + config[CONF_DEVICE_CLASS], + 10.0, + signer, + ) + + if not aftv.available: + # Determine the name that will be used for the device in the log + if CONF_NAME in config: + device_name = config[CONF_NAME] + elif config[CONF_DEVICE_CLASS] == DEVICE_ANDROIDTV: + device_name = "Android TV device" + elif config[CONF_DEVICE_CLASS] == DEVICE_FIRETV: + device_name = "Fire TV device" + else: + device_name = "Android TV / Fire TV device" + + _LOGGER.warning( + "Could not connect to %s at %s %s", device_name, address, adb_log + ) + raise PlatformNotReady + + async def _async_close(event): + """Close the ADB socket connection when HA stops.""" + await aftv.adb_close() + + # Close the ADB connection when HA stops + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_close) + + device_args = [ + aftv, + config[CONF_NAME], + config[CONF_APPS], + config[CONF_GET_SOURCES], + config.get(CONF_TURN_ON_COMMAND), + config.get(CONF_TURN_OFF_COMMAND), + config[CONF_EXCLUDE_UNNAMED_APPS], + config[CONF_SCREENCAP], + ] + + if aftv.DEVICE_CLASS == DEVICE_ANDROIDTV: + device = AndroidTVDevice(*device_args) + device_name = config.get(CONF_NAME, "Android TV") + else: + device = FireTVDevice(*device_args) + device_name = config.get(CONF_NAME, "Fire TV") + + async_add_entities([device]) + _LOGGER.debug("Setup %s at %s %s", device_name, address, adb_log) + hass.data[ANDROIDTV_DOMAIN][address] = device + + if hass.services.has_service(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND): + return + + platform = entity_platform.async_get_current_platform() + + async def service_adb_command(service): + """Dispatch service calls to target entities.""" + cmd = service.data[ATTR_COMMAND] + entity_id = service.data[ATTR_ENTITY_ID] + target_devices = [ + dev + for dev in hass.data[ANDROIDTV_DOMAIN].values() + if dev.entity_id in entity_id + ] + + for target_device in target_devices: + output = await target_device.adb_command(cmd) + + # log the output, if there is any + if output: + _LOGGER.info( + "Output of command '%s' from '%s': %s", + cmd, + target_device.entity_id, + output, + ) + + hass.services.async_register( + ANDROIDTV_DOMAIN, + SERVICE_ADB_COMMAND, + service_adb_command, + schema=SERVICE_ADB_COMMAND_SCHEMA, + ) + + platform.async_register_entity_service( + SERVICE_LEARN_SENDEVENT, {}, "learn_sendevent" + ) + + async def service_download(service): + """Download a file from your Android TV / Fire TV device to your Home Assistant instance.""" + local_path = service.data[ATTR_LOCAL_PATH] + if not hass.config.is_allowed_path(local_path): + _LOGGER.warning("'%s' is not secure to load data from!", local_path) + return + + device_path = service.data[ATTR_DEVICE_PATH] + entity_id = service.data[ATTR_ENTITY_ID] + target_device = [ + dev + for dev in hass.data[ANDROIDTV_DOMAIN].values() + if dev.entity_id in entity_id + ][0] + + await target_device.adb_pull(local_path, device_path) + + hass.services.async_register( + ANDROIDTV_DOMAIN, + SERVICE_DOWNLOAD, + service_download, + schema=SERVICE_DOWNLOAD_SCHEMA, + ) + + async def service_upload(service): + """Upload a file from your Home Assistant instance to an Android TV / Fire TV device.""" + local_path = service.data[ATTR_LOCAL_PATH] + if not hass.config.is_allowed_path(local_path): + _LOGGER.warning("'%s' is not secure to load data from!", local_path) + return + + device_path = service.data[ATTR_DEVICE_PATH] + entity_id = service.data[ATTR_ENTITY_ID] + target_devices = [ + dev + for dev in hass.data[ANDROIDTV_DOMAIN].values() + if dev.entity_id in entity_id + ] + + for target_device in target_devices: + await target_device.adb_push(local_path, device_path) + + hass.services.async_register( + ANDROIDTV_DOMAIN, SERVICE_UPLOAD, service_upload, schema=SERVICE_UPLOAD_SCHEMA + ) + + +def adb_decorator(override_available=False): + """Wrap ADB methods and catch exceptions. + + Allows for overriding the available status of the ADB connection via the + `override_available` parameter. + """ + + def _adb_decorator(func): + """Wrap the provided ADB method and catch exceptions.""" + + @functools.wraps(func) + async def _adb_exception_catcher(self, *args, **kwargs): + """Call an ADB-related method and catch exceptions.""" + if not self.available and not override_available: + return None + + try: + return await func(self, *args, **kwargs) + except LockNotAcquiredException: + # If the ADB lock could not be acquired, skip this command + _LOGGER.info( + "ADB command not executed because the connection is currently in use" + ) + return + except self.exceptions as err: + _LOGGER.error( + "Failed to execute an ADB command. ADB connection re-" + "establishing attempt in the next update. Error: %s", + err, + ) + await self.aftv.adb_close() + self._available = False + return None + except Exception: + # An unforeseen exception occurred. Close the ADB connection so that + # it doesn't happen over and over again, then raise the exception. + await self.aftv.adb_close() + self._available = False + raise + + return _adb_exception_catcher + + return _adb_decorator + + +class ADBDevice(MediaPlayerEntity): + """Representation of an Android TV or Fire TV device.""" + + def __init__( + self, + aftv, + name, + apps, + get_sources, + turn_on_command, + turn_off_command, + exclude_unnamed_apps, + screencap, + ): + """Initialize the Android TV / Fire TV device.""" + self.aftv = aftv + self._name = name + self._app_id_to_name = APPS.copy() + self._app_id_to_name.update(apps) + self._app_name_to_id = { + value: key for key, value in self._app_id_to_name.items() if value + } + + # Make sure that apps overridden via the `apps` parameter are reflected + # in `self._app_name_to_id` + for key, value in apps.items(): + self._app_name_to_id[value] = key + + self._get_sources = get_sources + self._keys = KEYS + + self._device_properties = self.aftv.device_properties + self._unique_id = self._device_properties.get("serialno") + + self.turn_on_command = turn_on_command + self.turn_off_command = turn_off_command + + self._exclude_unnamed_apps = exclude_unnamed_apps + self._screencap = screencap + + # ADB exceptions to catch + if not self.aftv.adb_server_ip: + # Using "adb_shell" (Python ADB implementation) + self.exceptions = ( + AdbTimeoutError, + BrokenPipeError, + ConnectionResetError, + ValueError, + InvalidChecksumError, + InvalidCommandError, + InvalidResponseError, + TcpTimeoutException, + ) + else: + # Using "pure-python-adb" (communicate with ADB server) + self.exceptions = (ConnectionResetError, RuntimeError) + + # Property attributes + self._adb_response = None + self._available = True + self._current_app = None + self._sources = None + self._state = None + self._hdmi_input = None + + @property + def app_id(self): + """Return the current app.""" + return self._current_app + + @property + def app_name(self): + """Return the friendly name of the current app.""" + return self._app_id_to_name.get(self._current_app, self._current_app) + + @property + def available(self): + """Return whether or not the ADB connection is valid.""" + return self._available + + @property + def extra_state_attributes(self): + """Provide the last ADB command's response and the device's HDMI input as attributes.""" + return { + "adb_response": self._adb_response, + "hdmi_input": self._hdmi_input, + } + + @property + def media_image_hash(self): + """Hash value for media image.""" + return f"{datetime.now().timestamp()}" if self._screencap else None + + @property + def name(self): + """Return the device name.""" + return self._name + + @property + def source(self): + """Return the current app.""" + return self._app_id_to_name.get(self._current_app, self._current_app) + + @property + def source_list(self): + """Return a list of running apps.""" + return self._sources + + @property + def state(self): + """Return the state of the player.""" + return self._state + + @property + def unique_id(self): + """Return the device unique id.""" + return self._unique_id + + @adb_decorator() + async def _adb_screencap(self): + """Take a screen capture from the device.""" + return await self.aftv.adb_screencap() + + async def async_get_media_image(self): + """Fetch current playing image.""" + if not self._screencap or self.state in [STATE_OFF, None] or not self.available: + return None, None + + media_data = await self._adb_screencap() + if media_data: + return media_data, "image/png" + + # If an exception occurred and the device is no longer available, write the state + if not self.available: + self.async_write_ha_state() + + return None, None + + @adb_decorator() + async def async_media_play(self): + """Send play command.""" + await self.aftv.media_play() + + @adb_decorator() + async def async_media_pause(self): + """Send pause command.""" + await self.aftv.media_pause() + + @adb_decorator() + async def async_media_play_pause(self): + """Send play/pause command.""" + await self.aftv.media_play_pause() + + @adb_decorator() + async def async_turn_on(self): + """Turn on the device.""" + if self.turn_on_command: + await self.aftv.adb_shell(self.turn_on_command) + else: + await self.aftv.turn_on() + + @adb_decorator() + async def async_turn_off(self): + """Turn off the device.""" + if self.turn_off_command: + await self.aftv.adb_shell(self.turn_off_command) + else: + await self.aftv.turn_off() + + @adb_decorator() + async def async_media_previous_track(self): + """Send previous track command (results in rewind).""" + await self.aftv.media_previous_track() + + @adb_decorator() + async def async_media_next_track(self): + """Send next track command (results in fast-forward).""" + await self.aftv.media_next_track() + + @adb_decorator() + async def async_select_source(self, source): + """Select input source. + + If the source starts with a '!', then it will close the app instead of + opening it. + """ + if isinstance(source, str): + if not source.startswith("!"): + await self.aftv.launch_app(self._app_name_to_id.get(source, source)) + else: + source_ = source[1:].lstrip() + await self.aftv.stop_app(self._app_name_to_id.get(source_, source_)) + + @adb_decorator() + async def adb_command(self, cmd): + """Send an ADB command to an Android TV / Fire TV device.""" + key = self._keys.get(cmd) + if key: + await self.aftv.adb_shell(f"input keyevent {key}") + return + + if cmd == "GET_PROPERTIES": + self._adb_response = str(await self.aftv.get_properties_dict()) + self.async_write_ha_state() + return self._adb_response + + try: + response = await self.aftv.adb_shell(cmd) + except UnicodeDecodeError: + return + + if isinstance(response, str) and response.strip(): + self._adb_response = response.strip() + self.async_write_ha_state() + + return self._adb_response + + @adb_decorator() + async def learn_sendevent(self): + """Translate a key press on a remote to ADB 'sendevent' commands.""" + output = await self.aftv.learn_sendevent() + if output: + self._adb_response = output + self.async_write_ha_state() + + msg = f"Output from service '{SERVICE_LEARN_SENDEVENT}' from {self.entity_id}: '{output}'" + self.hass.components.persistent_notification.async_create( + msg, + title="Android TV", + ) + _LOGGER.info("%s", msg) + + @adb_decorator() + async def adb_pull(self, local_path, device_path): + """Download a file from your Android TV / Fire TV device to your Home Assistant instance.""" + await self.aftv.adb_pull(local_path, device_path) + + @adb_decorator() + async def adb_push(self, local_path, device_path): + """Upload a file from your Home Assistant instance to an Android TV / Fire TV device.""" + await self.aftv.adb_push(local_path, device_path) + + +class AndroidTVDevice(ADBDevice): + """Representation of an Android TV device.""" + + def __init__( + self, + aftv, + name, + apps, + get_sources, + turn_on_command, + turn_off_command, + exclude_unnamed_apps, + screencap, + ): + """Initialize the Android TV device.""" + super().__init__( + aftv, + name, + apps, + get_sources, + turn_on_command, + turn_off_command, + exclude_unnamed_apps, + screencap, + ) + + self._is_volume_muted = None + self._volume_level = None + + @adb_decorator(override_available=True) + async def async_update(self): + """Update the device state and, if necessary, re-connect.""" + # Check if device is disconnected. + if not self._available: + # Try to connect + self._available = await self.aftv.adb_connect(always_log_errors=False) + + # If the ADB connection is not intact, don't update. + if not self._available: + return + + # Get the updated state and attributes. + ( + state, + self._current_app, + running_apps, + _, + self._is_volume_muted, + self._volume_level, + self._hdmi_input, + ) = await self.aftv.update(self._get_sources) + + self._state = ANDROIDTV_STATES.get(state) + if self._state is None: + self._available = False + + if running_apps: + sources = [ + self._app_id_to_name.get( + app_id, app_id if not self._exclude_unnamed_apps else None + ) + for app_id in running_apps + ] + self._sources = [source for source in sources if source] + else: + self._sources = None + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._is_volume_muted + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_ANDROIDTV + + @property + def volume_level(self): + """Return the volume level.""" + return self._volume_level + + @adb_decorator() + async def async_media_stop(self): + """Send stop command.""" + await self.aftv.media_stop() + + @adb_decorator() + async def async_mute_volume(self, mute): + """Mute the volume.""" + await self.aftv.mute_volume() + + @adb_decorator() + async def async_set_volume_level(self, volume): + """Set the volume level.""" + await self.aftv.set_volume_level(volume) + + @adb_decorator() + async def async_volume_down(self): + """Send volume down command.""" + self._volume_level = await self.aftv.volume_down(self._volume_level) + + @adb_decorator() + async def async_volume_up(self): + """Send volume up command.""" + self._volume_level = await self.aftv.volume_up(self._volume_level) + + +class FireTVDevice(ADBDevice): + """Representation of a Fire TV device.""" + + @adb_decorator(override_available=True) + async def async_update(self): + """Update the device state and, if necessary, re-connect.""" + # Check if device is disconnected. + if not self._available: + # Try to connect + self._available = await self.aftv.adb_connect(always_log_errors=False) + + # If the ADB connection is not intact, don't update. + if not self._available: + return + + # Get the `state`, `current_app`, `running_apps` and `hdmi_input`. + ( + state, + self._current_app, + running_apps, + self._hdmi_input, + ) = await self.aftv.update(self._get_sources) + + self._state = ANDROIDTV_STATES.get(state) + if self._state is None: + self._available = False + + if running_apps: + sources = [ + self._app_id_to_name.get( + app_id, app_id if not self._exclude_unnamed_apps else None + ) + for app_id in running_apps + ] + self._sources = [source for source in sources if source] + else: + self._sources = None + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_FIRETV + + @adb_decorator() + async def async_media_stop(self): + """Send stop (back) command.""" + await self.aftv.back() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/androidtv/services.yaml b/homeassistant-2021.6.0.dev0/homeassistant/components/androidtv/services.yaml new file mode 100644 index 00000000000..55b871ff58f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/androidtv/services.yaml @@ -0,0 +1,80 @@ +# Describes the format for available Android TV and Fire TV services + +adb_command: + name: ADB command + description: Send an ADB command to an Android TV / Fire TV device. + fields: + entity_id: + description: Name(s) of Android TV / Fire TV entities. + required: true + example: "media_player.android_tv_living_room" + selector: + entity: + integration: androidtv + domain: media_player + command: + name: Command + description: Either a key command or an ADB shell command. + required: true + example: "HOME" + selector: + text: +download: + name: Download + description: Download a file from your Android TV / Fire TV device to your Home Assistant instance. + fields: + entity_id: + description: Name of Android TV / Fire TV entity. + required: true + example: "media_player.android_tv_living_room" + selector: + entity: + integration: androidtv + domain: media_player + device_path: + name: Device path + description: The filepath on the Android TV / Fire TV device. + required: true + example: "/storage/emulated/0/Download/example.txt" + selector: + text: + local_path: + name: Local path + description: The filepath on your Home Assistant instance. + required: true + example: "/config/www/example.txt" + selector: + text: +upload: + name: Upload + description: Upload a file from your Home Assistant instance to an Android TV / Fire TV device. + fields: + entity_id: + description: Name(s) of Android TV / Fire TV entities. + required: true + example: "media_player.android_tv_living_room" + selector: + entity: + integration: androidtv + domain: media_player + device_path: + name: Device path + description: The filepath on the Android TV / Fire TV device. + required: true + example: "/storage/emulated/0/Download/example.txt" + selector: + text: + local_path: + name: Local path + description: The filepath on your Home Assistant instance. + required: true + example: "/config/www/example.txt" + selector: + text: +learn_sendevent: + name: Learn sendevent + description: Translate a key press on a remote into ADB 'sendevent' commands. You must press one button on the remote within 8 seconds of calling this service. + target: + entity: + integration: androidtv + domain: media_player diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/anel_pwrctrl/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/anel_pwrctrl/__init__.py new file mode 100644 index 00000000000..bd06aa87b36 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/anel_pwrctrl/__init__.py @@ -0,0 +1 @@ +"""The anel_pwrctrl component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/anel_pwrctrl/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/anel_pwrctrl/manifest.json new file mode 100644 index 00000000000..926549f768d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/anel_pwrctrl/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "anel_pwrctrl", + "name": "Anel NET-PwrCtrl", + "documentation": "https://www.home-assistant.io/integrations/anel_pwrctrl", + "requirements": ["anel_pwrctrl-homeassistant==0.0.1.dev2"], + "codeowners": [], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/anel_pwrctrl/switch.py b/homeassistant-2021.6.0.dev0/homeassistant/components/anel_pwrctrl/switch.py new file mode 100644 index 00000000000..0669a3bb6c6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/anel_pwrctrl/switch.py @@ -0,0 +1,107 @@ +"""Support for ANEL PwrCtrl switches.""" +from datetime import timedelta +import logging + +from anel_pwrctrl import DeviceMaster +import voluptuous as vol + +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +CONF_PORT_RECV = "port_recv" +CONF_PORT_SEND = "port_send" + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_PORT_RECV): cv.port, + vol.Required(CONF_PORT_SEND): cv.port, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_HOST): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up PwrCtrl devices/switches.""" + host = config.get(CONF_HOST) + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + port_recv = config[CONF_PORT_RECV] + port_send = config[CONF_PORT_SEND] + + try: + master = DeviceMaster( + username=username, + password=password, + read_port=port_send, + write_port=port_recv, + ) + master.query(ip_addr=host) + except OSError as ex: + _LOGGER.error("Unable to discover PwrCtrl device: %s", str(ex)) + return False + + devices = [] + for device in master.devices.values(): + parent_device = PwrCtrlDevice(device) + devices.extend( + PwrCtrlSwitch(switch, parent_device) for switch in device.switches.values() + ) + + add_entities(devices) + + +class PwrCtrlSwitch(SwitchEntity): + """Representation of a PwrCtrl switch.""" + + def __init__(self, port, parent_device): + """Initialize the PwrCtrl switch.""" + self._port = port + self._parent_device = parent_device + + @property + def unique_id(self): + """Return the unique ID of the device.""" + return f"{self._port.device.host}-{self._port.get_index()}" + + @property + def name(self): + """Return the name of the device.""" + return self._port.label + + @property + def is_on(self): + """Return true if the device is on.""" + return self._port.get_state() + + def update(self): + """Trigger update for all switches on the parent device.""" + self._parent_device.update() + + def turn_on(self, **kwargs): + """Turn the switch on.""" + self._port.on() + + def turn_off(self, **kwargs): + """Turn the switch off.""" + self._port.off() + + +class PwrCtrlDevice: + """Device representation for per device throttling.""" + + def __init__(self, device): + """Initialize the PwrCtrl device.""" + self._device = device + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Update the device and all its switches.""" + self._device.update() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/anthemav/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/anthemav/__init__.py new file mode 100644 index 00000000000..56b06e865c2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/anthemav/__init__.py @@ -0,0 +1 @@ +"""The anthemav component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/anthemav/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/anthemav/manifest.json new file mode 100644 index 00000000000..3e11675fa1f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/anthemav/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "anthemav", + "name": "Anthem A/V Receivers", + "documentation": "https://www.home-assistant.io/integrations/anthemav", + "requirements": ["anthemav==1.1.10"], + "codeowners": [], + "iot_class": "local_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/anthemav/media_player.py b/homeassistant-2021.6.0.dev0/homeassistant/components/anthemav/media_player.py new file mode 100644 index 00000000000..788fa8db7eb --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/anthemav/media_player.py @@ -0,0 +1,189 @@ +"""Support for Anthem Network Receivers and Processors.""" +import logging + +import anthemav +import voluptuous as vol + +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity +from homeassistant.components.media_player.const import ( + SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, +) +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PORT, + EVENT_HOMEASSISTANT_STOP, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "anthemav" + +DEFAULT_PORT = 14999 + +SUPPORT_ANTHEMAV = ( + SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_MUTE + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_SELECT_SOURCE +) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up our socket to the AVR.""" + + host = config[CONF_HOST] + port = config[CONF_PORT] + name = config.get(CONF_NAME) + device = None + + _LOGGER.info("Provisioning Anthem AVR device at %s:%d", host, port) + + @callback + def async_anthemav_update_callback(message): + """Receive notification from transport that new data exists.""" + _LOGGER.debug("Received update callback from AVR: %s", message) + async_dispatcher_send(hass, DOMAIN) + + avr = await anthemav.Connection.create( + host=host, port=port, update_callback=async_anthemav_update_callback + ) + + device = AnthemAVR(avr, name) + + _LOGGER.debug("dump_devicedata: %s", device.dump_avrdata) + _LOGGER.debug("dump_conndata: %s", avr.dump_conndata) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.avr.close) + async_add_entities([device]) + + +class AnthemAVR(MediaPlayerEntity): + """Entity reading values from Anthem AVR protocol.""" + + def __init__(self, avr, name): + """Initialize entity with transport.""" + super().__init__() + self.avr = avr + self._name = name + + def _lookup(self, propname, dval=None): + return getattr(self.avr.protocol, propname, dval) + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + async_dispatcher_connect(self.hass, DOMAIN, self.async_write_ha_state) + ) + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_ANTHEMAV + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return name of device.""" + return self._name or self._lookup("model") + + @property + def state(self): + """Return state of power on/off.""" + pwrstate = self._lookup("power") + + if pwrstate is True: + return STATE_ON + if pwrstate is False: + return STATE_OFF + return None + + @property + def is_volume_muted(self): + """Return boolean reflecting mute state on device.""" + return self._lookup("mute", False) + + @property + def volume_level(self): + """Return volume level from 0 to 1.""" + return self._lookup("volume_as_percentage", 0.0) + + @property + def media_title(self): + """Return current input name (closest we have to media title).""" + return self._lookup("input_name", "No Source") + + @property + def app_name(self): + """Return details about current video and audio stream.""" + return ( + f"{self._lookup('video_input_resolution_text', '')} " + f"{self._lookup('audio_input_name', '')}" + ) + + @property + def source(self): + """Return currently selected input.""" + return self._lookup("input_name", "Unknown") + + @property + def source_list(self): + """Return all active, configured inputs.""" + return self._lookup("input_list", ["Unknown"]) + + async def async_select_source(self, source): + """Change AVR to the designated source (by name).""" + self._update_avr("input_name", source) + + async def async_turn_off(self): + """Turn AVR power off.""" + self._update_avr("power", False) + + async def async_turn_on(self): + """Turn AVR power on.""" + self._update_avr("power", True) + + async def async_set_volume_level(self, volume): + """Set AVR volume (0 to 1).""" + self._update_avr("volume_as_percentage", volume) + + async def async_mute_volume(self, mute): + """Engage AVR mute.""" + self._update_avr("mute", mute) + + def _update_avr(self, propname, value): + """Update a property in the AVR.""" + _LOGGER.info("Sending command to AVR: set %s to %s", propname, str(value)) + setattr(self.avr.protocol, propname, value) + + @property + def dump_avrdata(self): + """Return state of avr object for debugging forensics.""" + attrs = vars(self) + items_string = ", ".join(f"{item}: {item}" for item in attrs.items()) + return f"dump_avrdata: {items_string}" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apache_kafka/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/apache_kafka/__init__.py new file mode 100644 index 00000000000..5be3732757f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apache_kafka/__init__.py @@ -0,0 +1,142 @@ +"""Support for Apache Kafka.""" +from datetime import datetime +import json + +from aiokafka import AIOKafkaProducer +import voluptuous as vol + +from homeassistant.const import ( + CONF_IP_ADDRESS, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, + EVENT_STATE_CHANGED, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entityfilter import FILTER_SCHEMA +from homeassistant.util import ssl as ssl_util + +DOMAIN = "apache_kafka" + +CONF_FILTER = "filter" +CONF_TOPIC = "topic" +CONF_SECURITY_PROTOCOL = "security_protocol" + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Required(CONF_PORT): cv.port, + vol.Required(CONF_TOPIC): cv.string, + vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, + vol.Optional(CONF_SECURITY_PROTOCOL, default="PLAINTEXT"): vol.In( + ["PLAINTEXT", "SASL_SSL"] + ), + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Activate the Apache Kafka integration.""" + conf = config[DOMAIN] + + kafka = hass.data[DOMAIN] = KafkaManager( + hass, + conf[CONF_IP_ADDRESS], + conf[CONF_PORT], + conf[CONF_TOPIC], + conf[CONF_FILTER], + conf[CONF_SECURITY_PROTOCOL], + conf.get(CONF_USERNAME), + conf.get(CONF_PASSWORD), + ) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, kafka.shutdown) + + await kafka.start() + + return True + + +class DateTimeJSONEncoder(json.JSONEncoder): + """Encode python objects. + + Additionally add encoding for datetime objects as isoformat. + """ + + def default(self, o): + """Implement encoding logic.""" + if isinstance(o, datetime): + return o.isoformat() + return super().default(o) + + +class KafkaManager: + """Define a manager to buffer events to Kafka.""" + + def __init__( + self, + hass, + ip_address, + port, + topic, + entities_filter, + security_protocol, + username, + password, + ): + """Initialize.""" + self._encoder = DateTimeJSONEncoder() + self._entities_filter = entities_filter + self._hass = hass + ssl_context = ssl_util.client_context() + self._producer = AIOKafkaProducer( + loop=hass.loop, + bootstrap_servers=f"{ip_address}:{port}", + compression_type="gzip", + security_protocol=security_protocol, + ssl_context=ssl_context, + sasl_mechanism="PLAIN", + sasl_plain_username=username, + sasl_plain_password=password, + ) + self._topic = topic + + def _encode_event(self, event): + """Translate events into a binary JSON payload.""" + state = event.data.get("new_state") + if ( + state is None + or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE) + or not self._entities_filter(state.entity_id) + ): + return + + return json.dumps(obj=state.as_dict(), default=self._encoder.encode).encode( + "utf-8" + ) + + async def start(self): + """Start the Kafka manager.""" + self._hass.bus.async_listen(EVENT_STATE_CHANGED, self.write) + await self._producer.start() + + async def shutdown(self, _): + """Shut the manager down.""" + await self._producer.stop() + + async def write(self, event): + """Write a binary payload to Kafka.""" + payload = self._encode_event(event) + + if payload: + await self._producer.send_and_wait(self._topic, payload) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apache_kafka/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apache_kafka/manifest.json new file mode 100644 index 00000000000..688c7c9fb3d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apache_kafka/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "apache_kafka", + "name": "Apache Kafka", + "documentation": "https://www.home-assistant.io/integrations/apache_kafka", + "requirements": ["aiokafka==0.6.0"], + "codeowners": ["@bachya"], + "iot_class": "local_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apcupsd/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/apcupsd/__init__.py new file mode 100644 index 00000000000..181f70d725a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apcupsd/__init__.py @@ -0,0 +1,85 @@ +"""Support for APCUPSd via its Network Information Server (NIS).""" +from datetime import timedelta +import logging + +from apcaccess import status +import voluptuous as vol + +from homeassistant.const import CONF_HOST, CONF_PORT +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_HOST = "localhost" +DEFAULT_PORT = 3551 +DOMAIN = "apcupsd" + +KEY_STATUS = "STATFLAG" + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + +VALUE_ONLINE = 8 + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Use config values to set up a function enabling status retrieval.""" + conf = config[DOMAIN] + host = conf[CONF_HOST] + port = conf[CONF_PORT] + + apcups_data = APCUPSdData(host, port) + hass.data[DOMAIN] = apcups_data + + # It doesn't really matter why we're not able to get the status, just that + # we can't. + try: + apcups_data.update(no_throttle=True) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Failure while testing APCUPSd status retrieval") + return False + return True + + +class APCUPSdData: + """Stores the data retrieved from APCUPSd. + + For each entity to use, acts as the single point responsible for fetching + updates from the server. + """ + + def __init__(self, host, port): + """Initialize the data object.""" + + self._host = host + self._port = port + self._status = None + self._get = status.get + self._parse = status.parse + + @property + def status(self): + """Get latest update if throttle allows. Return status.""" + self.update() + return self._status + + def _get_status(self): + """Get the status from APCUPSd and parse it into a dict.""" + return self._parse(self._get(host=self._host, port=self._port)) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self, **kwargs): + """Fetch the latest status from APCUPSd.""" + self._status = self._get_status() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/apcupsd/binary_sensor.py new file mode 100644 index 00000000000..daf9592f3e6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apcupsd/binary_sensor.py @@ -0,0 +1,44 @@ +"""Support for tracking the online status of a UPS.""" +import voluptuous as vol + +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv + +from . import DOMAIN, KEY_STATUS, VALUE_ONLINE + +DEFAULT_NAME = "UPS Online Status" +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string} +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up an APCUPSd Online Status binary sensor.""" + apcups_data = hass.data[DOMAIN] + + add_entities([OnlineStatus(config, apcups_data)], True) + + +class OnlineStatus(BinarySensorEntity): + """Representation of an UPS online status.""" + + def __init__(self, config, data): + """Initialize the APCUPSd binary device.""" + self._config = config + self._data = data + self._state = None + + @property + def name(self): + """Return the name of the UPS online status sensor.""" + return self._config[CONF_NAME] + + @property + def is_on(self): + """Return true if the UPS is online, else false.""" + return self._state & VALUE_ONLINE > 0 + + def update(self): + """Get the status report from APCUPSd and set this entity's state.""" + self._state = int(self._data.status[KEY_STATUS], 16) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apcupsd/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apcupsd/manifest.json new file mode 100644 index 00000000000..ac9352bae44 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apcupsd/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "apcupsd", + "name": "apcupsd", + "documentation": "https://www.home-assistant.io/integrations/apcupsd", + "requirements": ["apcaccess==0.0.13"], + "codeowners": [], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apcupsd/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/apcupsd/sensor.py new file mode 100644 index 00000000000..36dc1155b7f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apcupsd/sensor.py @@ -0,0 +1,200 @@ +"""Support for APCUPSd sensors.""" +import logging + +from apcaccess.status import ALL_UNITS +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.const import ( + CONF_RESOURCES, + ELECTRICAL_CURRENT_AMPERE, + ELECTRICAL_VOLT_AMPERE, + FREQUENCY_HERTZ, + PERCENTAGE, + POWER_WATT, + TEMP_CELSIUS, + TIME_MINUTES, + TIME_SECONDS, + VOLT, +) +import homeassistant.helpers.config_validation as cv + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SENSOR_PREFIX = "UPS " +SENSOR_TYPES = { + "alarmdel": ["Alarm Delay", "", "mdi:alarm"], + "ambtemp": ["Ambient Temperature", "", "mdi:thermometer"], + "apc": ["Status Data", "", "mdi:information-outline"], + "apcmodel": ["Model", "", "mdi:information-outline"], + "badbatts": ["Bad Batteries", "", "mdi:information-outline"], + "battdate": ["Battery Replaced", "", "mdi:calendar-clock"], + "battstat": ["Battery Status", "", "mdi:information-outline"], + "battv": ["Battery Voltage", VOLT, "mdi:flash"], + "bcharge": ["Battery", PERCENTAGE, "mdi:battery"], + "cable": ["Cable Type", "", "mdi:ethernet-cable"], + "cumonbatt": ["Total Time on Battery", "", "mdi:timer-outline"], + "date": ["Status Date", "", "mdi:calendar-clock"], + "dipsw": ["Dip Switch Settings", "", "mdi:information-outline"], + "dlowbatt": ["Low Battery Signal", "", "mdi:clock-alert"], + "driver": ["Driver", "", "mdi:information-outline"], + "dshutd": ["Shutdown Delay", "", "mdi:timer-outline"], + "dwake": ["Wake Delay", "", "mdi:timer-outline"], + "endapc": ["Date and Time", "", "mdi:calendar-clock"], + "extbatts": ["External Batteries", "", "mdi:information-outline"], + "firmware": ["Firmware Version", "", "mdi:information-outline"], + "hitrans": ["Transfer High", VOLT, "mdi:flash"], + "hostname": ["Hostname", "", "mdi:information-outline"], + "humidity": ["Ambient Humidity", PERCENTAGE, "mdi:water-percent"], + "itemp": ["Internal Temperature", TEMP_CELSIUS, "mdi:thermometer"], + "lastxfer": ["Last Transfer", "", "mdi:transfer"], + "linefail": ["Input Voltage Status", "", "mdi:information-outline"], + "linefreq": ["Line Frequency", FREQUENCY_HERTZ, "mdi:information-outline"], + "linev": ["Input Voltage", VOLT, "mdi:flash"], + "loadpct": ["Load", PERCENTAGE, "mdi:gauge"], + "loadapnt": ["Load Apparent Power", PERCENTAGE, "mdi:gauge"], + "lotrans": ["Transfer Low", VOLT, "mdi:flash"], + "mandate": ["Manufacture Date", "", "mdi:calendar"], + "masterupd": ["Master Update", "", "mdi:information-outline"], + "maxlinev": ["Input Voltage High", VOLT, "mdi:flash"], + "maxtime": ["Battery Timeout", "", "mdi:timer-off-outline"], + "mbattchg": ["Battery Shutdown", PERCENTAGE, "mdi:battery-alert"], + "minlinev": ["Input Voltage Low", VOLT, "mdi:flash"], + "mintimel": ["Shutdown Time", "", "mdi:timer-outline"], + "model": ["Model", "", "mdi:information-outline"], + "nombattv": ["Battery Nominal Voltage", VOLT, "mdi:flash"], + "nominv": ["Nominal Input Voltage", VOLT, "mdi:flash"], + "nomoutv": ["Nominal Output Voltage", VOLT, "mdi:flash"], + "nompower": ["Nominal Output Power", POWER_WATT, "mdi:flash"], + "nomapnt": ["Nominal Apparent Power", ELECTRICAL_VOLT_AMPERE, "mdi:flash"], + "numxfers": ["Transfer Count", "", "mdi:counter"], + "outcurnt": ["Output Current", ELECTRICAL_CURRENT_AMPERE, "mdi:flash"], + "outputv": ["Output Voltage", VOLT, "mdi:flash"], + "reg1": ["Register 1 Fault", "", "mdi:information-outline"], + "reg2": ["Register 2 Fault", "", "mdi:information-outline"], + "reg3": ["Register 3 Fault", "", "mdi:information-outline"], + "retpct": ["Restore Requirement", PERCENTAGE, "mdi:battery-alert"], + "selftest": ["Last Self Test", "", "mdi:calendar-clock"], + "sense": ["Sensitivity", "", "mdi:information-outline"], + "serialno": ["Serial Number", "", "mdi:information-outline"], + "starttime": ["Startup Time", "", "mdi:calendar-clock"], + "statflag": ["Status Flag", "", "mdi:information-outline"], + "status": ["Status", "", "mdi:information-outline"], + "stesti": ["Self Test Interval", "", "mdi:information-outline"], + "timeleft": ["Time Left", "", "mdi:clock-alert"], + "tonbatt": ["Time on Battery", "", "mdi:timer-outline"], + "upsmode": ["Mode", "", "mdi:information-outline"], + "upsname": ["Name", "", "mdi:information-outline"], + "version": ["Daemon Info", "", "mdi:information-outline"], + "xoffbat": ["Transfer from Battery", "", "mdi:transfer"], + "xoffbatt": ["Transfer from Battery", "", "mdi:transfer"], + "xonbatt": ["Transfer to Battery", "", "mdi:transfer"], +} + +SPECIFIC_UNITS = {"ITEMP": TEMP_CELSIUS} +INFERRED_UNITS = { + " Minutes": TIME_MINUTES, + " Seconds": TIME_SECONDS, + " Percent": PERCENTAGE, + " Volts": VOLT, + " Ampere": ELECTRICAL_CURRENT_AMPERE, + " Volt-Ampere": ELECTRICAL_VOLT_AMPERE, + " Watts": POWER_WATT, + " Hz": FREQUENCY_HERTZ, + " C": TEMP_CELSIUS, + " Percent Load Capacity": PERCENTAGE, +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_RESOURCES, default=[]): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ) + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the APCUPSd sensors.""" + apcups_data = hass.data[DOMAIN] + entities = [] + + for resource in config[CONF_RESOURCES]: + sensor_type = resource.lower() + + if sensor_type not in SENSOR_TYPES: + SENSOR_TYPES[sensor_type] = [ + sensor_type.title(), + "", + "mdi:information-outline", + ] + + if sensor_type.upper() not in apcups_data.status: + _LOGGER.warning( + "Sensor type: %s does not appear in the APCUPSd status output", + sensor_type, + ) + + entities.append(APCUPSdSensor(apcups_data, sensor_type)) + + add_entities(entities, True) + + +def infer_unit(value): + """If the value ends with any of the units from ALL_UNITS. + + Split the unit off the end of the value and return the value, unit tuple + pair. Else return the original value and None as the unit. + """ + + for unit in ALL_UNITS: + if value.endswith(unit): + return value[: -len(unit)], INFERRED_UNITS.get(unit, unit.strip()) + return value, None + + +class APCUPSdSensor(SensorEntity): + """Representation of a sensor entity for APCUPSd status values.""" + + def __init__(self, data, sensor_type): + """Initialize the sensor.""" + self._data = data + self.type = sensor_type + self._name = SENSOR_PREFIX + SENSOR_TYPES[sensor_type][0] + self._unit = SENSOR_TYPES[sensor_type][1] + self._inferred_unit = None + self._state = None + + @property + def name(self): + """Return the name of the UPS sensor.""" + return self._name + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return SENSOR_TYPES[self.type][2] + + @property + def state(self): + """Return true if the UPS is online, else False.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + if not self._unit: + return self._inferred_unit + return self._unit + + 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._state = None + self._inferred_unit = None + else: + self._state, self._inferred_unit = infer_unit( + self._data.status[self.type.upper()] + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/api/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/api/__init__.py new file mode 100644 index 00000000000..a91d8540286 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/api/__init__.py @@ -0,0 +1,448 @@ +"""Rest API for Home Assistant.""" +import asyncio +from contextlib import suppress +import json +import logging + +from aiohttp import web +from aiohttp.web_exceptions import HTTPBadRequest +import async_timeout +import voluptuous as vol + +from homeassistant.auth.permissions.const import POLICY_READ +from homeassistant.bootstrap import DATA_LOGGING +from homeassistant.components.http import HomeAssistantView +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, + EVENT_TIME_CHANGED, + HTTP_BAD_REQUEST, + HTTP_CREATED, + HTTP_NOT_FOUND, + HTTP_OK, + MATCH_ALL, + URL_API, + URL_API_COMPONENTS, + URL_API_CONFIG, + URL_API_DISCOVERY_INFO, + URL_API_ERROR_LOG, + URL_API_EVENTS, + URL_API_SERVICES, + URL_API_STATES, + URL_API_STREAM, + URL_API_TEMPLATE, + __version__, +) +import homeassistant.core as ha +from homeassistant.exceptions import ServiceNotFound, TemplateError, Unauthorized +from homeassistant.helpers import template +from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.network import NoURLAvailableError, get_url +from homeassistant.helpers.service import async_get_all_descriptions +from homeassistant.helpers.system_info import async_get_system_info + +_LOGGER = logging.getLogger(__name__) + +ATTR_BASE_URL = "base_url" +ATTR_EXTERNAL_URL = "external_url" +ATTR_INTERNAL_URL = "internal_url" +ATTR_LOCATION_NAME = "location_name" +ATTR_INSTALLATION_TYPE = "installation_type" +ATTR_REQUIRES_API_PASSWORD = "requires_api_password" +ATTR_UUID = "uuid" +ATTR_VERSION = "version" + +DOMAIN = "api" +STREAM_PING_PAYLOAD = "ping" +STREAM_PING_INTERVAL = 50 # seconds + + +async def async_setup(hass, config): + """Register the API with the HTTP interface.""" + hass.http.register_view(APIStatusView) + hass.http.register_view(APIEventStream) + hass.http.register_view(APIConfigView) + hass.http.register_view(APIDiscoveryView) + hass.http.register_view(APIStatesView) + hass.http.register_view(APIEntityStateView) + hass.http.register_view(APIEventListenersView) + hass.http.register_view(APIEventView) + hass.http.register_view(APIServicesView) + hass.http.register_view(APIDomainServicesView) + hass.http.register_view(APIComponentsView) + hass.http.register_view(APITemplateView) + + if DATA_LOGGING in hass.data: + hass.http.register_view(APIErrorLog) + + return True + + +class APIStatusView(HomeAssistantView): + """View to handle Status requests.""" + + url = URL_API + name = "api:status" + + @ha.callback + def get(self, request): + """Retrieve if API is running.""" + return self.json_message("API running.") + + +class APIEventStream(HomeAssistantView): + """View to handle EventStream requests.""" + + url = URL_API_STREAM + name = "api:stream" + + async def get(self, request): + """Provide a streaming interface for the event bus.""" + if not request["hass_user"].is_admin: + raise Unauthorized() + hass = request.app["hass"] + stop_obj = object() + to_write = asyncio.Queue() + + restrict = request.query.get("restrict") + if restrict: + restrict = restrict.split(",") + [EVENT_HOMEASSISTANT_STOP] + + async def forward_events(event): + """Forward events to the open request.""" + if event.event_type == EVENT_TIME_CHANGED: + return + + if restrict and event.event_type not in restrict: + return + + _LOGGER.debug("STREAM %s FORWARDING %s", id(stop_obj), event) + + if event.event_type == EVENT_HOMEASSISTANT_STOP: + data = stop_obj + else: + data = json.dumps(event, cls=JSONEncoder) + + await to_write.put(data) + + response = web.StreamResponse() + response.content_type = "text/event-stream" + await response.prepare(request) + + unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events) + + try: + _LOGGER.debug("STREAM %s ATTACHED", id(stop_obj)) + + # Fire off one message so browsers fire open event right away + await to_write.put(STREAM_PING_PAYLOAD) + + while True: + try: + with async_timeout.timeout(STREAM_PING_INTERVAL): + payload = await to_write.get() + + if payload is stop_obj: + break + + msg = f"data: {payload}\n\n" + _LOGGER.debug("STREAM %s WRITING %s", id(stop_obj), msg.strip()) + await response.write(msg.encode("UTF-8")) + except asyncio.TimeoutError: + await to_write.put(STREAM_PING_PAYLOAD) + + except asyncio.CancelledError: + _LOGGER.debug("STREAM %s ABORT", id(stop_obj)) + + finally: + _LOGGER.debug("STREAM %s RESPONSE CLOSED", id(stop_obj)) + unsub_stream() + + return response + + +class APIConfigView(HomeAssistantView): + """View to handle Configuration requests.""" + + url = URL_API_CONFIG + name = "api:config" + + @ha.callback + def get(self, request): + """Get current configuration.""" + return self.json(request.app["hass"].config.as_dict()) + + +class APIDiscoveryView(HomeAssistantView): + """View to provide Discovery information.""" + + requires_auth = False + url = URL_API_DISCOVERY_INFO + name = "api:discovery" + + async def get(self, request): + """Get discovery information.""" + hass = request.app["hass"] + uuid = await hass.helpers.instance_id.async_get() + system_info = await async_get_system_info(hass) + + data = { + ATTR_UUID: uuid, + ATTR_BASE_URL: None, + ATTR_EXTERNAL_URL: None, + ATTR_INTERNAL_URL: None, + ATTR_LOCATION_NAME: hass.config.location_name, + ATTR_INSTALLATION_TYPE: system_info[ATTR_INSTALLATION_TYPE], + # always needs authentication + ATTR_REQUIRES_API_PASSWORD: True, + ATTR_VERSION: __version__, + } + + with suppress(NoURLAvailableError): + data["external_url"] = get_url(hass, allow_internal=False) + + with suppress(NoURLAvailableError): + data["internal_url"] = get_url(hass, allow_external=False) + + # Set old base URL based on external or internal + data["base_url"] = data["external_url"] or data["internal_url"] + + return self.json(data) + + +class APIStatesView(HomeAssistantView): + """View to handle States requests.""" + + url = URL_API_STATES + name = "api:states" + + @ha.callback + def get(self, request): + """Get current states.""" + user = request["hass_user"] + entity_perm = user.permissions.check_entity + states = [ + state + for state in request.app["hass"].states.async_all() + if entity_perm(state.entity_id, "read") + ] + return self.json(states) + + +class APIEntityStateView(HomeAssistantView): + """View to handle EntityState requests.""" + + url = "/api/states/{entity_id}" + name = "api:entity-state" + + @ha.callback + def get(self, request, entity_id): + """Retrieve state of entity.""" + user = request["hass_user"] + if not user.permissions.check_entity(entity_id, POLICY_READ): + raise Unauthorized(entity_id=entity_id) + + state = request.app["hass"].states.get(entity_id) + if state: + return self.json(state) + return self.json_message("Entity not found.", HTTP_NOT_FOUND) + + async def post(self, request, entity_id): + """Update state of entity.""" + if not request["hass_user"].is_admin: + raise Unauthorized(entity_id=entity_id) + hass = request.app["hass"] + try: + data = await request.json() + except ValueError: + return self.json_message("Invalid JSON specified.", HTTP_BAD_REQUEST) + + new_state = data.get("state") + + if new_state is None: + return self.json_message("No state specified.", HTTP_BAD_REQUEST) + + attributes = data.get("attributes") + force_update = data.get("force_update", False) + + is_new_state = hass.states.get(entity_id) is None + + # Write state + hass.states.async_set( + entity_id, new_state, attributes, force_update, self.context(request) + ) + + # Read the state back for our response + status_code = HTTP_CREATED if is_new_state else HTTP_OK + resp = self.json(hass.states.get(entity_id), status_code) + + resp.headers.add("Location", f"/api/states/{entity_id}") + + return resp + + @ha.callback + def delete(self, request, entity_id): + """Remove entity.""" + if not request["hass_user"].is_admin: + raise Unauthorized(entity_id=entity_id) + if request.app["hass"].states.async_remove(entity_id): + return self.json_message("Entity removed.") + return self.json_message("Entity not found.", HTTP_NOT_FOUND) + + +class APIEventListenersView(HomeAssistantView): + """View to handle EventListeners requests.""" + + url = URL_API_EVENTS + name = "api:event-listeners" + + @ha.callback + def get(self, request): + """Get event listeners.""" + return self.json(async_events_json(request.app["hass"])) + + +class APIEventView(HomeAssistantView): + """View to handle Event requests.""" + + url = "/api/events/{event_type}" + name = "api:event" + + async def post(self, request, event_type): + """Fire events.""" + if not request["hass_user"].is_admin: + raise Unauthorized() + body = await request.text() + try: + event_data = json.loads(body) if body else None + except ValueError: + return self.json_message( + "Event data should be valid JSON.", HTTP_BAD_REQUEST + ) + + if event_data is not None and not isinstance(event_data, dict): + return self.json_message( + "Event data should be a JSON object", HTTP_BAD_REQUEST + ) + + # Special case handling for event STATE_CHANGED + # We will try to convert state dicts back to State objects + if event_type == ha.EVENT_STATE_CHANGED and event_data: + for key in ("old_state", "new_state"): + state = ha.State.from_dict(event_data.get(key)) + + if state: + event_data[key] = state + + request.app["hass"].bus.async_fire( + event_type, event_data, ha.EventOrigin.remote, self.context(request) + ) + + return self.json_message(f"Event {event_type} fired.") + + +class APIServicesView(HomeAssistantView): + """View to handle Services requests.""" + + url = URL_API_SERVICES + name = "api:services" + + async def get(self, request): + """Get registered services.""" + services = await async_services_json(request.app["hass"]) + return self.json(services) + + +class APIDomainServicesView(HomeAssistantView): + """View to handle DomainServices requests.""" + + url = "/api/services/{domain}/{service}" + name = "api:domain-services" + + async def post(self, request, domain, service): + """Call a service. + + Returns a list of changed states. + """ + hass: ha.HomeAssistant = request.app["hass"] + body = await request.text() + try: + data = json.loads(body) if body else None + except ValueError: + return self.json_message("Data should be valid JSON.", HTTP_BAD_REQUEST) + + context = self.context(request) + + try: + await hass.services.async_call( + domain, service, data, blocking=True, context=context + ) + except (vol.Invalid, ServiceNotFound) as ex: + raise HTTPBadRequest() from ex + + changed_states = [] + + for state in hass.states.async_all(): + if state.context is context: + changed_states.append(state) + + return self.json(changed_states) + + +class APIComponentsView(HomeAssistantView): + """View to handle Components requests.""" + + url = URL_API_COMPONENTS + name = "api:components" + + @ha.callback + def get(self, request): + """Get current loaded components.""" + return self.json(request.app["hass"].config.components) + + +class APITemplateView(HomeAssistantView): + """View to handle Template requests.""" + + url = URL_API_TEMPLATE + name = "api:template" + + async def post(self, request): + """Render a template.""" + if not request["hass_user"].is_admin: + raise Unauthorized() + try: + data = await request.json() + tpl = template.Template(data["template"], request.app["hass"]) + return tpl.async_render(variables=data.get("variables"), parse_result=False) + except (ValueError, TemplateError) as ex: + return self.json_message( + f"Error rendering template: {ex}", HTTP_BAD_REQUEST + ) + + +class APIErrorLog(HomeAssistantView): + """View to fetch the API error log.""" + + url = URL_API_ERROR_LOG + name = "api:error_log" + + async def get(self, request): + """Retrieve API error log.""" + if not request["hass_user"].is_admin: + raise Unauthorized() + return web.FileResponse(request.app["hass"].data[DATA_LOGGING]) + + +async def async_services_json(hass): + """Generate services data to JSONify.""" + descriptions = await async_get_all_descriptions(hass) + return [{"domain": key, "services": value} for key, value in descriptions.items()] + + +@ha.callback +def async_events_json(hass): + """Generate event data to JSONify.""" + return [ + {"event": key, "listener_count": value} + for key, value in hass.bus.async_listeners().items() + ] diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/api/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/api/manifest.json new file mode 100644 index 00000000000..1f400470943 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/api/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "api", + "name": "Home Assistant API", + "documentation": "https://www.home-assistant.io/integrations/api", + "dependencies": ["http"], + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/api/services.yaml b/homeassistant-2021.6.0.dev0/homeassistant/components/api/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apns/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/apns/__init__.py new file mode 100644 index 00000000000..9332b0d1ede --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apns/__init__.py @@ -0,0 +1 @@ +"""The apns component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apns/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/apns/const.py new file mode 100644 index 00000000000..a8dc1204aa1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apns/const.py @@ -0,0 +1,2 @@ +"""Constants for the apns component.""" +DOMAIN = "apns" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apns/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apns/manifest.json new file mode 100644 index 00000000000..73136a2ff29 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apns/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "apns", + "name": "Apple Push Notification Service (APNS)", + "documentation": "https://www.home-assistant.io/integrations/apns", + "requirements": ["apns2==0.3.0"], + "after_dependencies": ["device_tracker"], + "codeowners": [], + "iot_class": "cloud_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apns/notify.py b/homeassistant-2021.6.0.dev0/homeassistant/components/apns/notify.py new file mode 100644 index 00000000000..c9e12a20863 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apns/notify.py @@ -0,0 +1,263 @@ +"""APNS Notification platform.""" +from contextlib import suppress +import logging + +from apns2.client import APNsClient +from apns2.errors import Unregistered +from apns2.payload import Payload +import voluptuous as vol + +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_TARGET, + PLATFORM_SCHEMA, + BaseNotificationService, +) +from homeassistant.config import load_yaml_config_file +from homeassistant.const import ATTR_NAME, CONF_NAME, CONF_PLATFORM +from homeassistant.helpers import template as template_helper +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_state_change + +from .const import DOMAIN + +APNS_DEVICES = "apns.yaml" +CONF_CERTFILE = "cert_file" +CONF_TOPIC = "topic" +CONF_SANDBOX = "sandbox" + +ATTR_PUSH_ID = "push_id" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_PLATFORM): "apns", + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_CERTFILE): cv.isfile, + vol.Required(CONF_TOPIC): cv.string, + vol.Optional(CONF_SANDBOX, default=False): cv.boolean, + } +) + +REGISTER_SERVICE_SCHEMA = vol.Schema( + {vol.Required(ATTR_PUSH_ID): cv.string, vol.Optional(ATTR_NAME): cv.string} +) + + +def get_service(hass, config, discovery_info=None): + """Return push service.""" + name = config[CONF_NAME] + cert_file = config[CONF_CERTFILE] + topic = config[CONF_TOPIC] + sandbox = config[CONF_SANDBOX] + + service = ApnsNotificationService(hass, name, topic, sandbox, cert_file) + hass.services.register( + DOMAIN, f"apns_{name}", service.register, schema=REGISTER_SERVICE_SCHEMA + ) + return service + + +class ApnsDevice: + """ + The APNS Device class. + + Stores information about a device that is registered for push + notifications. + """ + + def __init__(self, push_id, name, tracking_device_id=None, disabled=False): + """Initialize APNS Device.""" + self.device_push_id = push_id + self.device_name = name + self.tracking_id = tracking_device_id + self.device_disabled = disabled + + @property + def push_id(self): + """Return the APNS id for the device.""" + return self.device_push_id + + @property + def name(self): + """Return the friendly name for the device.""" + return self.device_name + + @property + def tracking_device_id(self): + """ + Return the device Id. + + The id of a device that is tracked by the device + tracking component. + """ + return self.tracking_id + + @property + def full_tracking_device_id(self): + """ + Return the fully qualified device id. + + The full id of a device that is tracked by the device + tracking component. + """ + return f"{DEVICE_TRACKER_DOMAIN}.{self.tracking_id}" + + @property + def disabled(self): + """Return the state of the service.""" + return self.device_disabled + + def disable(self): + """Disable the device from receiving notifications.""" + self.device_disabled = True + + def __eq__(self, other): + """Return the comparison.""" + if isinstance(other, self.__class__): + return self.push_id == other.push_id and self.name == other.name + return NotImplemented + + def __ne__(self, other): + """Return the comparison.""" + return not self.__eq__(other) + + +def _write_device(out, device): + """Write a single device to file.""" + attributes = [] + if device.name is not None: + attributes.append(f"name: {device.name}") + if device.tracking_device_id is not None: + attributes.append(f"tracking_device_id: {device.tracking_device_id}") + if device.disabled: + attributes.append("disabled: True") + + out.write(device.push_id) + out.write(": {") + if attributes: + separator = ", " + out.write(separator.join(attributes)) + + out.write("}\n") + + +class ApnsNotificationService(BaseNotificationService): + """Implement the notification service for the APNS service.""" + + def __init__(self, hass, app_name, topic, sandbox, cert_file): + """Initialize APNS application.""" + self.hass = hass + self.app_name = app_name + self.sandbox = sandbox + self.certificate = cert_file + self.yaml_path = hass.config.path(f"{app_name}_{APNS_DEVICES}") + self.devices = {} + self.device_states = {} + self.topic = topic + + with suppress(FileNotFoundError): + self.devices = { + str(key): ApnsDevice( + str(key), + value.get("name"), + value.get("tracking_device_id"), + value.get("disabled", False), + ) + for (key, value) in load_yaml_config_file(self.yaml_path).items() + } + + tracking_ids = [ + device.full_tracking_device_id + for (key, device) in self.devices.items() + if device.tracking_device_id is not None + ] + track_state_change(hass, tracking_ids, self.device_state_changed_listener) + + def device_state_changed_listener(self, entity_id, from_s, to_s): + """ + Listen for state change. + + Track device state change if a device has a tracking id specified. + """ + self.device_states[entity_id] = str(to_s.state) + + def write_devices(self): + """Write all known devices to file.""" + with open(self.yaml_path, "w+") as out: + for device in self.devices.values(): + _write_device(out, device) + + def register(self, call): + """Register a device to receive push messages.""" + push_id = call.data.get(ATTR_PUSH_ID) + + device_name = call.data.get(ATTR_NAME) + current_device = self.devices.get(push_id) + current_tracking_id = ( + None if current_device is None else current_device.tracking_device_id + ) + + device = ApnsDevice(push_id, device_name, current_tracking_id) + + if current_device is None: + self.devices[push_id] = device + with open(self.yaml_path, "a") as out: + _write_device(out, device) + return True + + if device != current_device: + self.devices[push_id] = device + self.write_devices() + + return True + + def send_message(self, message=None, **kwargs): + """Send push message to registered devices.""" + + apns = APNsClient( + self.certificate, use_sandbox=self.sandbox, use_alternative_port=False + ) + + device_state = kwargs.get(ATTR_TARGET) + message_data = kwargs.get(ATTR_DATA) + + if message_data is None: + message_data = {} + + if isinstance(message, str): + rendered_message = message + elif isinstance(message, template_helper.Template): + rendered_message = message.render(parse_result=False) + else: + rendered_message = "" + + payload = Payload( + alert=rendered_message, + badge=message_data.get("badge"), + sound=message_data.get("sound"), + category=message_data.get("category"), + custom=message_data.get("custom", {}), + content_available=message_data.get("content_available", False), + ) + + device_update = False + + for push_id, device in self.devices.items(): + if not device.disabled: + state = None + if device.tracking_device_id is not None: + state = self.device_states.get(device.full_tracking_device_id) + + if device_state is None or state == str(device_state): + try: + apns.send_notification(push_id, payload, topic=self.topic) + except Unregistered: + logging.error("Device %s has unregistered", push_id) + device_update = True + device.disable() + + if device_update: + self.write_devices() + + return True diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apns/services.yaml b/homeassistant-2021.6.0.dev0/homeassistant/components/apns/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/__init__.py new file mode 100644 index 00000000000..a1bd50ab221 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/__init__.py @@ -0,0 +1,385 @@ +"""The Apple TV integration.""" +import asyncio +import logging +from random import randrange + +from pyatv import connect, exceptions, scan +from pyatv.const import Protocol + +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH +from homeassistant.const import ( + CONF_ADDRESS, + CONF_NAME, + CONF_PROTOCOL, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import Entity + +from .const import CONF_CREDENTIALS, CONF_IDENTIFIER, CONF_START_OFF, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "Apple TV" + +BACKOFF_TIME_UPPER_LIMIT = 300 # Five minutes + +NOTIFICATION_TITLE = "Apple TV Notification" +NOTIFICATION_ID = "apple_tv_notification" + +SIGNAL_CONNECTED = "apple_tv_connected" +SIGNAL_DISCONNECTED = "apple_tv_disconnected" + +PLATFORMS = [MP_DOMAIN, REMOTE_DOMAIN] + + +async def async_setup_entry(hass, entry): + """Set up a config entry for Apple TV.""" + manager = AppleTVManager(hass, entry) + hass.data.setdefault(DOMAIN, {})[entry.unique_id] = manager + + async def on_hass_stop(event): + """Stop push updates when hass stops.""" + await manager.disconnect() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + ) + + async def setup_platforms(): + """Set up platforms and initiate connection.""" + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_setup(entry, platform) + for platform in PLATFORMS + ] + ) + await manager.init() + + hass.async_create_task(setup_platforms()) + + return True + + +async def async_unload_entry(hass, entry): + """Unload an Apple TV config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + if unload_ok: + manager = hass.data[DOMAIN].pop(entry.unique_id) + await manager.disconnect() + + return unload_ok + + +class AppleTVEntity(Entity): + """Device that sends commands to an Apple TV.""" + + def __init__(self, name, identifier, manager): + """Initialize device.""" + self.atv = None + self.manager = manager + self._name = name + self._identifier = identifier + + async def async_added_to_hass(self): + """Handle when an entity is about to be added to Home Assistant.""" + + @callback + def _async_connected(atv): + """Handle that a connection was made to a device.""" + self.atv = atv + self.async_device_connected(atv) + self.async_write_ha_state() + + @callback + def _async_disconnected(): + """Handle that a connection to a device was lost.""" + self.async_device_disconnected() + self.atv = None + self.async_write_ha_state() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, f"{SIGNAL_CONNECTED}_{self._identifier}", _async_connected + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SIGNAL_DISCONNECTED}_{self._identifier}", + _async_disconnected, + ) + ) + + def async_device_connected(self, atv): + """Handle when connection is made to device.""" + + def async_device_disconnected(self): + """Handle when connection was lost to device.""" + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def unique_id(self): + """Return a unique ID.""" + return self._identifier + + @property + def should_poll(self): + """No polling needed for Apple TV.""" + return False + + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self._identifier)}, + } + + +class AppleTVManager: + """Connection and power manager for an Apple TV. + + An instance is used per device to share the same power state between + several platforms. It also manages scanning and connection establishment + in case of problems. + """ + + def __init__(self, hass, config_entry): + """Initialize power manager.""" + self.config_entry = config_entry + self.hass = hass + self.atv = None + self._is_on = not config_entry.options.get(CONF_START_OFF, False) + self._connection_attempts = 0 + self._connection_was_lost = False + self._task = None + + async def init(self): + """Initialize power management.""" + if self._is_on: + await self.connect() + + def connection_lost(self, _): + """Device was unexpectedly disconnected. + + This is a callback function from pyatv.interface.DeviceListener. + """ + _LOGGER.warning( + 'Connection lost to Apple TV "%s"', self.config_entry.data[CONF_NAME] + ) + self._connection_was_lost = True + self._handle_disconnect() + + def connection_closed(self): + """Device connection was (intentionally) closed. + + This is a callback function from pyatv.interface.DeviceListener. + """ + self._handle_disconnect() + + def _handle_disconnect(self): + """Handle that the device disconnected and restart connect loop.""" + if self.atv: + self.atv.listener = None + self.atv.close() + self.atv = None + self._dispatch_send(SIGNAL_DISCONNECTED) + self._start_connect_loop() + + async def connect(self): + """Connect to device.""" + self._is_on = True + self._start_connect_loop() + + async def disconnect(self): + """Disconnect from device.""" + _LOGGER.debug("Disconnecting from device") + self._is_on = False + try: + if self.atv: + self.atv.push_updater.listener = None + self.atv.push_updater.stop() + self.atv.close() + self.atv = None + if self._task: + self._task.cancel() + self._task = None + except Exception: # pylint: disable=broad-except + _LOGGER.exception("An error occurred while disconnecting") + + def _start_connect_loop(self): + """Start background connect loop to device.""" + if not self._task and self.atv is None and self._is_on: + self._task = asyncio.create_task(self._connect_loop()) + else: + _LOGGER.debug( + "Not starting connect loop (%s, %s)", self.atv is None, self._is_on + ) + + async def _connect_loop(self): + """Connect loop background task function.""" + _LOGGER.debug("Starting connect loop") + + # Try to find device and connect as long as the user has said that + # we are allowed to connect and we are not already connected. + while self._is_on and self.atv is None: + try: + conf = await self._scan() + if conf: + await self._connect(conf) + except exceptions.AuthenticationError: + self._auth_problem() + break + except asyncio.CancelledError: + pass + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Failed to connect") + self.atv = None + + if self.atv is None: + self._connection_attempts += 1 + backoff = min( + randrange(2 ** self._connection_attempts), BACKOFF_TIME_UPPER_LIMIT + ) + + _LOGGER.debug("Reconnecting in %d seconds", backoff) + await asyncio.sleep(backoff) + + _LOGGER.debug("Connect loop ended") + self._task = None + + def _auth_problem(self): + """Problem to authenticate occurred that needs intervention.""" + _LOGGER.debug("Authentication error, reconfigure integration") + + name = self.config_entry.data[CONF_NAME] + identifier = self.config_entry.unique_id + + self.hass.components.persistent_notification.create( + "An irrecoverable connection problem occurred when connecting to " + f"`f{name}`. Please go to the Integrations page and reconfigure it", + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID, + ) + + # Add to event queue as this function is called from a task being + # cancelled from disconnect + asyncio.create_task(self.disconnect()) + + self.hass.async_create_task( + self.hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={CONF_NAME: name, CONF_IDENTIFIER: identifier}, + ) + ) + + async def _scan(self): + """Try to find device by scanning for it.""" + identifier = self.config_entry.unique_id + address = self.config_entry.data[CONF_ADDRESS] + protocol = Protocol(self.config_entry.data[CONF_PROTOCOL]) + + _LOGGER.debug("Discovering device %s", identifier) + atvs = await scan( + self.hass.loop, identifier=identifier, protocol=protocol, hosts=[address] + ) + if atvs: + return atvs[0] + + _LOGGER.debug( + "Failed to find device %s with address %s, trying to scan", + identifier, + address, + ) + + atvs = await scan(self.hass.loop, identifier=identifier, protocol=protocol) + if atvs: + return atvs[0] + + _LOGGER.debug("Failed to find device %s, trying later", identifier) + + return None + + async def _connect(self, conf): + """Connect to device.""" + credentials = self.config_entry.data[CONF_CREDENTIALS] + session = async_get_clientsession(self.hass) + + for protocol, creds in credentials.items(): + conf.set_credentials(Protocol(int(protocol)), creds) + + _LOGGER.debug("Connecting to device %s", self.config_entry.data[CONF_NAME]) + self.atv = await connect(conf, self.hass.loop, session=session) + self.atv.listener = self + + self._dispatch_send(SIGNAL_CONNECTED, self.atv) + self._address_updated(str(conf.address)) + + await self._async_setup_device_registry() + + self._connection_attempts = 0 + if self._connection_was_lost: + _LOGGER.info( + 'Connection was re-established to Apple TV "%s"', + self.config_entry.data[CONF_NAME], + ) + self._connection_was_lost = False + + async def _async_setup_device_registry(self): + attrs = { + "identifiers": {(DOMAIN, self.config_entry.unique_id)}, + "manufacturer": "Apple", + "name": self.config_entry.data[CONF_NAME], + } + + area = attrs["name"] + name_trailer = f" {DEFAULT_NAME}" + if area.endswith(name_trailer): + area = area[: -len(name_trailer)] + attrs["suggested_area"] = area + + if self.atv: + dev_info = self.atv.device_info + + attrs["model"] = DEFAULT_NAME + " " + dev_info.model.name.replace("Gen", "") + attrs["sw_version"] = dev_info.version + + if dev_info.mac: + attrs["connections"] = {(dr.CONNECTION_NETWORK_MAC, dev_info.mac)} + + device_registry = await dr.async_get_registry(self.hass) + device_registry.async_get_or_create( + config_entry_id=self.config_entry.entry_id, **attrs + ) + + @property + def is_connecting(self): + """Return true if connection is in progress.""" + return self._task is not None + + def _address_updated(self, address): + """Update cached address in config entry.""" + _LOGGER.debug("Changing address to %s", address) + self.hass.config_entries.async_update_entry( + self.config_entry, data={**self.config_entry.data, CONF_ADDRESS: address} + ) + + def _dispatch_send(self, signal, *args): + """Dispatch a signal to all entities managed by this manager.""" + async_dispatcher_send( + self.hass, f"{signal}_{self.config_entry.unique_id}", *args + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/config_flow.py new file mode 100644 index 00000000000..9afcb7a61ca --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/config_flow.py @@ -0,0 +1,402 @@ +"""Config flow for Apple TV integration.""" +from ipaddress import ip_address +import logging +from random import randrange + +from pyatv import exceptions, pair, scan +from pyatv.const import Protocol +from pyatv.convert import protocol_str +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_ADDRESS, + CONF_NAME, + CONF_PIN, + CONF_PROTOCOL, + CONF_TYPE, +) +from homeassistant.core import callback +from homeassistant.data_entry_flow import AbortFlow +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CONF_CREDENTIALS, CONF_IDENTIFIER, CONF_START_OFF, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DEVICE_INPUT = "device_input" + +INPUT_PIN_SCHEMA = vol.Schema({vol.Required(CONF_PIN, default=None): int}) + +DEFAULT_START_OFF = False +PROTOCOL_PRIORITY = [Protocol.MRP, Protocol.DMAP, Protocol.AirPlay] + + +async def device_scan(identifier, loop, cache=None): + """Scan for a specific device using identifier as filter.""" + + def _filter_device(dev): + if identifier is None: + return True + if identifier == str(dev.address): + return True + if identifier == dev.name: + return True + return any(service.identifier == identifier for service in dev.services) + + def _host_filter(): + try: + return [ip_address(identifier)] + except ValueError: + return None + + if cache: + matches = [atv for atv in cache if _filter_device(atv)] + if matches: + return cache, matches[0] + + for hosts in [_host_filter(), None]: + scan_result = await scan(loop, timeout=3, hosts=hosts) + matches = [atv for atv in scan_result if _filter_device(atv)] + + if matches: + return scan_result, matches[0] + + return scan_result, None + + +def is_valid_credentials(credentials): + """Verify that credentials are valid for establishing a connection.""" + return ( + credentials.get(Protocol.MRP.value) is not None + or credentials.get(Protocol.DMAP.value) is not None + ) + + +class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Apple TV.""" + + VERSION = 1 + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get options flow for this handler.""" + return AppleTVOptionsFlow(config_entry) + + def __init__(self): + """Initialize a new AppleTVConfigFlow.""" + self.target_device = None + self.scan_result = None + self.atv = None + self.protocol = None + self.pairing = None + self.credentials = {} # Protocol -> credentials + + async def async_step_reauth(self, info): + """Handle initial step when updating invalid credentials.""" + await self.async_set_unique_id(info[CONF_IDENTIFIER]) + self.target_device = info[CONF_IDENTIFIER] + + self.context["title_placeholders"] = {"name": info[CONF_NAME]} + self.context["identifier"] = self.unique_id + return await self.async_step_reconfigure() + + async def async_step_reconfigure(self, user_input=None): + """Inform user that reconfiguration is about to start.""" + if user_input is not None: + return await self.async_find_device_wrapper( + self.async_begin_pairing, allow_exist=True + ) + + return self.async_show_form(step_id="reconfigure") + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + # Be helpful to the user and look for devices + if self.scan_result is None: + self.scan_result, _ = await device_scan(None, self.hass.loop) + + errors = {} + default_suggestion = self._prefill_identifier() + if user_input is not None: + self.target_device = user_input[DEVICE_INPUT] + try: + await self.async_find_device() + except DeviceNotFound: + errors["base"] = "no_devices_found" + except DeviceAlreadyConfigured: + errors["base"] = "already_configured" + except exceptions.NoServiceError: + errors["base"] = "no_usable_service" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id( + self.atv.identifier, raise_on_progress=False + ) + return await self.async_step_confirm() + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(DEVICE_INPUT, default=default_suggestion): str} + ), + errors=errors, + description_placeholders={"devices": self._devices_str()}, + ) + + async def async_step_zeroconf(self, discovery_info): + """Handle device found via zeroconf.""" + service_type = discovery_info[CONF_TYPE] + properties = discovery_info["properties"] + + if service_type == "_mediaremotetv._tcp.local.": + identifier = properties["UniqueIdentifier"] + name = properties["Name"] + elif service_type == "_touch-able._tcp.local.": + identifier = discovery_info["name"].split(".")[0] + name = properties["CtlN"] + else: + return self.async_abort(reason="unknown") + + await self.async_set_unique_id(identifier) + self._abort_if_unique_id_configured() + + self.context["identifier"] = self.unique_id + self.context["title_placeholders"] = {"name": name} + self.target_device = identifier + return await self.async_find_device_wrapper(self.async_step_confirm) + + async def async_find_device_wrapper(self, next_func, allow_exist=False): + """Find a specific device and call another function when done. + + This function will do error handling and bail out when an error + occurs. + """ + try: + await self.async_find_device(allow_exist) + except DeviceNotFound: + return self.async_abort(reason="no_devices_found") + except DeviceAlreadyConfigured: + return self.async_abort(reason="already_configured") + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return self.async_abort(reason="unknown") + + return await next_func() + + async def async_find_device(self, allow_exist=False): + """Scan for the selected device to discover services.""" + self.scan_result, self.atv = await device_scan( + self.target_device, self.hass.loop, cache=self.scan_result + ) + if not self.atv: + raise DeviceNotFound() + + self.protocol = self.atv.main_service().protocol + + if not allow_exist: + for identifier in self.atv.all_identifiers: + if identifier in self._async_current_ids(): + raise DeviceAlreadyConfigured() + + # If credentials were found, save them + for service in self.atv.services: + if service.credentials: + self.credentials[service.protocol.value] = service.credentials + + async def async_step_confirm(self, user_input=None): + """Handle user-confirmation of discovered node.""" + if user_input is not None: + return await self.async_begin_pairing() + return self.async_show_form( + step_id="confirm", description_placeholders={"name": self.atv.name} + ) + + async def async_begin_pairing(self): + """Start pairing process for the next available protocol.""" + self.protocol = self._next_protocol_to_pair() + + # Dispose previous pairing sessions + if self.pairing is not None: + await self.pairing.close() + self.pairing = None + + # Any more protocols to pair? Else bail out here + if not self.protocol: + await self.async_set_unique_id(self.atv.main_service().identifier) + return self._async_get_entry( + self.atv.main_service().protocol, + self.atv.name, + self.credentials, + self.atv.address, + ) + + # Initiate the pairing process + abort_reason = None + session = async_get_clientsession(self.hass) + self.pairing = await pair( + self.atv, self.protocol, self.hass.loop, session=session + ) + try: + await self.pairing.begin() + except exceptions.ConnectionFailedError: + return await self.async_step_service_problem() + except exceptions.BackOffError: + abort_reason = "backoff" + except exceptions.PairingError: + _LOGGER.exception("Authentication problem") + abort_reason = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + abort_reason = "unknown" + + if abort_reason: + if self.pairing: + await self.pairing.close() + return self.async_abort(reason=abort_reason) + + # Choose step depending on if PIN is required from user or not + if self.pairing.device_provides_pin: + return await self.async_step_pair_with_pin() + + return await self.async_step_pair_no_pin() + + async def async_step_pair_with_pin(self, user_input=None): + """Handle pairing step where a PIN is required from the user.""" + errors = {} + if user_input is not None: + try: + self.pairing.pin(user_input[CONF_PIN]) + await self.pairing.finish() + self.credentials[self.protocol.value] = self.pairing.service.credentials + return await self.async_begin_pairing() + except exceptions.PairingError: + _LOGGER.exception("Authentication problem") + errors["base"] = "invalid_auth" + except AbortFlow: + raise + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="pair_with_pin", + data_schema=INPUT_PIN_SCHEMA, + errors=errors, + description_placeholders={"protocol": protocol_str(self.protocol)}, + ) + + async def async_step_pair_no_pin(self, user_input=None): + """Handle step where user has to enter a PIN on the device.""" + if user_input is not None: + await self.pairing.finish() + if self.pairing.has_paired: + self.credentials[self.protocol.value] = self.pairing.service.credentials + return await self.async_begin_pairing() + + await self.pairing.close() + return self.async_abort(reason="device_did_not_pair") + + pin = randrange(1000, stop=10000) + self.pairing.pin(pin) + return self.async_show_form( + step_id="pair_no_pin", + description_placeholders={ + "protocol": protocol_str(self.protocol), + "pin": pin, + }, + ) + + async def async_step_service_problem(self, user_input=None): + """Inform user that a service will not be added.""" + if user_input is not None: + self.credentials[self.protocol.value] = None + return await self.async_begin_pairing() + + return self.async_show_form( + step_id="service_problem", + description_placeholders={"protocol": protocol_str(self.protocol)}, + ) + + def _async_get_entry(self, protocol, name, credentials, address): + if not is_valid_credentials(credentials): + return self.async_abort(reason="invalid_config") + + data = { + CONF_PROTOCOL: protocol.value, + CONF_NAME: name, + CONF_CREDENTIALS: credentials, + CONF_ADDRESS: str(address), + } + + self._abort_if_unique_id_configured(reload_on_update=False, updates=data) + + return self.async_create_entry(title=name, data=data) + + def _next_protocol_to_pair(self): + def _needs_pairing(protocol): + if self.atv.get_service(protocol) is None: + return False + return protocol.value not in self.credentials + + for protocol in PROTOCOL_PRIORITY: + if _needs_pairing(protocol): + return protocol + return None + + def _devices_str(self): + return ", ".join( + [ + f"`{atv.name} ({atv.address})`" + for atv in self.scan_result + if atv.identifier not in self._async_current_ids() + ] + ) + + def _prefill_identifier(self): + # Return identifier (address) of one device that has not been paired with + for atv in self.scan_result: + if atv.identifier not in self._async_current_ids(): + return str(atv.address) + return "" + + +class AppleTVOptionsFlow(config_entries.OptionsFlow): + """Handle Apple TV options.""" + + def __init__(self, config_entry): + """Initialize Apple TV options flow.""" + self.config_entry = config_entry + self.options = dict(config_entry.options) + + async def async_step_init(self, user_input=None): + """Manage the Apple TV options.""" + if user_input is not None: + self.options[CONF_START_OFF] = user_input[CONF_START_OFF] + return self.async_create_entry(title="", data=self.options) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_START_OFF, + default=self.config_entry.options.get( + CONF_START_OFF, DEFAULT_START_OFF + ), + ): bool, + } + ), + ) + + +class DeviceNotFound(HomeAssistantError): + """Error to indicate device could not be found.""" + + +class DeviceAlreadyConfigured(HomeAssistantError): + """Error to indicate device is already configured.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/const.py new file mode 100644 index 00000000000..ac04cc1b937 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/const.py @@ -0,0 +1,11 @@ +"""Constants for the Apple TV integration.""" + +DOMAIN = "apple_tv" + +CONF_IDENTIFIER = "identifier" +CONF_CREDENTIALS = "credentials" +CONF_CREDENTIALS_MRP = "mrp" +CONF_CREDENTIALS_DMAP = "dmap" +CONF_CREDENTIALS_AIRPLAY = "airplay" + +CONF_START_OFF = "start_off" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/manifest.json new file mode 100644 index 00000000000..963cbb9be33 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "apple_tv", + "name": "Apple TV", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/apple_tv", + "requirements": ["pyatv==0.7.7"], + "zeroconf": ["_mediaremotetv._tcp.local.", "_touch-able._tcp.local."], + "after_dependencies": ["discovery"], + "codeowners": ["@postlund"], + "iot_class": "local_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/media_player.py b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/media_player.py new file mode 100644 index 00000000000..a855fc6b53e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/media_player.py @@ -0,0 +1,312 @@ +"""Support for Apple TV media player.""" +import logging + +from pyatv.const import ( + DeviceState, + FeatureName, + FeatureState, + MediaType, + RepeatState, + ShuffleState, +) + +from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MUSIC, + MEDIA_TYPE_TVSHOW, + MEDIA_TYPE_VIDEO, + REPEAT_MODE_ALL, + REPEAT_MODE_OFF, + REPEAT_MODE_ONE, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_REPEAT_SET, + SUPPORT_SEEK, + SUPPORT_SHUFFLE_SET, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_STEP, +) +from homeassistant.const import ( + CONF_NAME, + STATE_IDLE, + STATE_OFF, + STATE_PAUSED, + STATE_PLAYING, + STATE_STANDBY, +) +from homeassistant.core import callback +import homeassistant.util.dt as dt_util + +from . import AppleTVEntity +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 0 + +SUPPORT_APPLE_TV = ( + SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_PLAY_MEDIA + | SUPPORT_PAUSE + | SUPPORT_PLAY + | SUPPORT_SEEK + | SUPPORT_STOP + | SUPPORT_NEXT_TRACK + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_VOLUME_STEP + | SUPPORT_REPEAT_SET + | SUPPORT_SHUFFLE_SET +) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Load Apple TV media player based on a config entry.""" + name = config_entry.data[CONF_NAME] + manager = hass.data[DOMAIN][config_entry.unique_id] + async_add_entities([AppleTvMediaPlayer(name, config_entry.unique_id, manager)]) + + +class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): + """Representation of an Apple TV media player.""" + + def __init__(self, name, identifier, manager, **kwargs): + """Initialize the Apple TV media player.""" + super().__init__(name, identifier, manager, **kwargs) + self._playing = None + + @callback + def async_device_connected(self, atv): + """Handle when connection is made to device.""" + self.atv.push_updater.listener = self + self.atv.push_updater.start() + + @callback + def async_device_disconnected(self): + """Handle when connection was lost to device.""" + self.atv.push_updater.stop() + self.atv.push_updater.listener = None + + @property + def state(self): + """Return the state of the device.""" + if self.manager.is_connecting: + return None + if self.atv is None: + return STATE_OFF + if self._playing: + state = self._playing.device_state + if state in (DeviceState.Idle, DeviceState.Loading): + return STATE_IDLE + if state == DeviceState.Playing: + return STATE_PLAYING + if state in (DeviceState.Paused, DeviceState.Seeking, DeviceState.Stopped): + return STATE_PAUSED + return STATE_STANDBY # Bad or unknown state? + return None + + @callback + def playstatus_update(self, _, playing): + """Print what is currently playing when it changes.""" + self._playing = playing + self.async_write_ha_state() + + @callback + def playstatus_error(self, _, exception): + """Inform about an error and restart push updates.""" + _LOGGER.warning("A %s error occurred: %s", exception.__class__, exception) + self._playing = None + self.async_write_ha_state() + + @property + def app_id(self): + """ID of the current running app.""" + if self._is_feature_available(FeatureName.App): + return self.atv.metadata.app.identifier + return None + + @property + def app_name(self): + """Name of the current running app.""" + if self._is_feature_available(FeatureName.App): + return self.atv.metadata.app.name + return None + + @property + def media_content_type(self): + """Content type of current playing media.""" + if self._playing: + return { + MediaType.Video: MEDIA_TYPE_VIDEO, + MediaType.Music: MEDIA_TYPE_MUSIC, + MediaType.TV: MEDIA_TYPE_TVSHOW, + }.get(self._playing.media_type) + return None + + @property + def media_duration(self): + """Duration of current playing media in seconds.""" + if self._playing: + return self._playing.total_time + return None + + @property + def media_position(self): + """Position of current playing media in seconds.""" + if self._playing: + return self._playing.position + return None + + @property + def media_position_updated_at(self): + """Last valid time of media position.""" + if self.state in (STATE_PLAYING, STATE_PAUSED): + return dt_util.utcnow() + return None + + async def async_play_media(self, media_type, media_id, **kwargs): + """Send the play_media command to the media player.""" + await self.atv.stream.play_url(media_id) + + @property + def media_image_hash(self): + """Hash value for media image.""" + state = self.state + if self._playing and state not in [None, STATE_OFF, STATE_IDLE]: + return self.atv.metadata.artwork_id + return None + + async def async_get_media_image(self): + """Fetch media image of current playing image.""" + state = self.state + if self._playing and state not in [STATE_OFF, STATE_IDLE]: + artwork = await self.atv.metadata.artwork() + if artwork: + return artwork.bytes, artwork.mimetype + + return None, None + + @property + def media_title(self): + """Title of current playing media.""" + if self._playing: + return self._playing.title + return None + + @property + def media_artist(self): + """Artist of current playing media, music track only.""" + if self._is_feature_available(FeatureName.Artist): + return self._playing.artist + return None + + @property + def media_album_name(self): + """Album name of current playing media, music track only.""" + if self._is_feature_available(FeatureName.Album): + return self._playing.album + return None + + @property + def repeat(self): + """Return current repeat mode.""" + if self._is_feature_available(FeatureName.Repeat): + return { + RepeatState.Track: REPEAT_MODE_ONE, + RepeatState.All: REPEAT_MODE_ALL, + }.get(self._playing.repeat, REPEAT_MODE_OFF) + return None + + @property + def shuffle(self): + """Boolean if shuffle is enabled.""" + if self._is_feature_available(FeatureName.Shuffle): + return self._playing.shuffle != ShuffleState.Off + return None + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_APPLE_TV + + def _is_feature_available(self, feature): + """Return if a feature is available.""" + if self.atv and self._playing: + return self.atv.features.in_state(FeatureState.Available, feature) + return False + + async def async_turn_on(self): + """Turn the media player on.""" + await self.manager.connect() + + async def async_turn_off(self): + """Turn the media player off.""" + self._playing = None + await self.manager.disconnect() + + async def async_media_play_pause(self): + """Pause media on media player.""" + if self._playing: + await self.atv.remote_control.play_pause() + return None + + async def async_media_play(self): + """Play media.""" + if self.atv: + await self.atv.remote_control.play() + + async def async_media_stop(self): + """Stop the media player.""" + if self.atv: + await self.atv.remote_control.stop() + + async def async_media_pause(self): + """Pause the media player.""" + if self.atv: + await self.atv.remote_control.pause() + + async def async_media_next_track(self): + """Send next track command.""" + if self.atv: + await self.atv.remote_control.next() + + async def async_media_previous_track(self): + """Send previous track command.""" + if self.atv: + await self.atv.remote_control.previous() + + async def async_media_seek(self, position): + """Send seek command.""" + if self.atv: + await self.atv.remote_control.set_position(position) + + async def async_volume_up(self): + """Turn volume up for media player.""" + if self.atv: + await self.atv.remote_control.volume_up() + + async def async_volume_down(self): + """Turn volume down for media player.""" + if self.atv: + await self.atv.remote_control.volume_down() + + async def async_set_repeat(self, repeat): + """Set repeat mode.""" + if self.atv: + mode = { + REPEAT_MODE_ONE: RepeatState.Track, + REPEAT_MODE_ALL: RepeatState.All, + }.get(repeat, RepeatState.Off) + await self.atv.remote_control.set_repeat(mode) + + async def async_set_shuffle(self, shuffle): + """Enable/disable shuffle mode.""" + if self.atv: + await self.atv.remote_control.set_shuffle( + ShuffleState.Songs if shuffle else ShuffleState.Off + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/remote.py b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/remote.py new file mode 100644 index 00000000000..3d88bddcbc9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/remote.py @@ -0,0 +1,67 @@ +"""Remote control support for Apple TV.""" + +import asyncio +import logging + +from homeassistant.components.remote import ( + ATTR_DELAY_SECS, + ATTR_NUM_REPEATS, + DEFAULT_DELAY_SECS, + RemoteEntity, +) +from homeassistant.const import CONF_NAME + +from . import AppleTVEntity +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Load Apple TV remote based on a config entry.""" + name = config_entry.data[CONF_NAME] + manager = hass.data[DOMAIN][config_entry.unique_id] + async_add_entities([AppleTVRemote(name, config_entry.unique_id, manager)]) + + +class AppleTVRemote(AppleTVEntity, RemoteEntity): + """Device that sends commands to an Apple TV.""" + + @property + def is_on(self): + """Return true if device is on.""" + return self.atv is not None + + @property + def should_poll(self): + """No polling needed for Apple TV.""" + return False + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + await self.manager.connect() + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + await self.manager.disconnect() + + async def async_send_command(self, command, **kwargs): + """Send a command to one device.""" + num_repeats = kwargs[ATTR_NUM_REPEATS] + delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) + + if not self.is_on: + _LOGGER.error("Unable to send commands, not connected to %s", self._name) + return + + for _ in range(num_repeats): + for single_command in command: + attr_value = getattr(self.atv.remote_control, single_command, None) + if not attr_value: + raise ValueError("Command not found. Exiting sequence") + + _LOGGER.info("Sending command %s", single_command) + await attr_value() + await asyncio.sleep(delay) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/strings.json new file mode 100644 index 00000000000..00dd92cac89 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/strings.json @@ -0,0 +1,64 @@ +{ + "title": "Apple TV", + "config": { + "flow_title": "{name}", + "step": { + "user": { + "title": "Setup a new Apple TV", + "description": "Start by entering the device name (e.g. Kitchen or Bedroom) or IP address of the Apple TV you want to add. If any devices were automatically found on your network, they are shown below.\n\nIf you cannot see your device or experience any issues, try specifying the device IP address.\n\n{devices}", + "data": { + "device_input": "Device" + } + }, + "reconfigure": { + "title": "Device reconfiguration", + "description": "This Apple TV is experiencing some connection difficulties and must be reconfigured." + }, + "pair_with_pin": { + "title": "Pairing", + "description": "Pairing is required for the `{protocol}` protocol. Please enter the PIN code displayed on screen. Leading zeros shall be omitted, i.e. enter 123 if the displayed code is 0123.", + "data": { + "pin": "[%key:common::config_flow::data::pin%]" + } + }, + "pair_no_pin": { + "title": "Pairing", + "description": "Pairing is required for the `{protocol}` service. Please enter PIN {pin} on your Apple TV to continue." + }, + "service_problem": { + "title": "Failed to add service", + "description": "A problem occurred while pairing protocol `{protocol}`. It will be ignored." + }, + "confirm": { + "title": "Confirm adding Apple TV", + "description": "You are about to add the Apple TV named `{name}` to Home Assistant.\n\n**To complete the process, you may have to enter multiple PIN codes.**\n\nPlease note that you will *not* be able to power off your Apple TV with this integration. Only the media player in Home Assistant will turn off!" + } + }, + "error": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "no_usable_service": "A device was found but could not identify any way to establish a connection to it. If you keep seeing this message, try specifying its IP address or restarting your Apple TV.", + "unknown": "[%key:common::config_flow::error::unknown%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", + "device_did_not_pair": "No attempt to finish pairing process was made from the device.", + "backoff": "Device does not accept pairing reqests at this time (you might have entered an invalid PIN code too many times), try again later.", + "invalid_config": "The configuration for this device is incomplete. Please try adding it again.", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + }, + "options": { + "step": { + "init": { + "description": "Configure general device settings", + "data": { + "start_off": "Do not turn device on when starting Home Assistant" + } + } + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/ca.json new file mode 100644 index 00000000000..646931135e2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/ca.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "backoff": "En aquests moments el dispositiu no accepta sol\u00b7licituds de vinculaci\u00f3 (\u00e9s possible que hagis introdu\u00eft un codi PIN inv\u00e0lid massa vegades), torna-ho a provar m\u00e9s tard.", + "device_did_not_pair": "No s'ha fet cap intent d'acabar el proc\u00e9s de vinculaci\u00f3 des del dispositiu.", + "invalid_config": "La configuraci\u00f3 d'aquest dispositiu no est\u00e0 completa. Intenta'l tornar a afegir.", + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "unknown": "Error inesperat" + }, + "error": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "no_usable_service": "S'ha trobat un dispositiu per\u00f2 no ha pogut identificar cap manera d'establir-hi una connexi\u00f3. Si continues veient aquest missatge, prova d'especificar-ne l'adre\u00e7a IP o reinicia l'Apple TV.", + "unknown": "Error inesperat" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Est\u00e0s a punt d'afegir l'Apple TV amb nom \"{name}\" a Home Assistant.\n\n **Per completar el proc\u00e9s, \u00e9s possible que hagis d'introduir alguns codis PIN.** \n\n Tingues en compte que *no* pots apagar la teva Apple TV a trav\u00e9s d'aquesta integraci\u00f3. Nom\u00e9s es desactivar\u00e0 el reproductor de Home Assistant.", + "title": "Confirma l'addici\u00f3 de l'Apple TV" + }, + "pair_no_pin": { + "description": "Vinculaci\u00f3 necess\u00e0ria amb el servei `{protocol}`. Per continuar, introdueix el PIN {pin} a la teva Apple TV.", + "title": "Vinculaci\u00f3" + }, + "pair_with_pin": { + "data": { + "pin": "Codi PIN" + }, + "description": "Amb el protocol \"{protocol}\" \u00e9s necess\u00e0ria la vinculaci\u00f3. Introdueix el codi PIN que es mostra en pantalla. Els zeros a l'inici, si n'hi ha, s'han d'ometre; per exemple: introdueix 123 si el codi mostrat \u00e9s 0123.", + "title": "Vinculaci\u00f3" + }, + "reconfigure": { + "description": "Aquesta Apple TV est\u00e0 tenint problemes de connexi\u00f3 i s'ha de tornar a configurar.", + "title": "Reconfiguraci\u00f3 de dispositiu" + }, + "service_problem": { + "description": "S'ha produ\u00eft un problema en la vinculaci\u00f3 protocol \"{protocol}\". S'ignorar\u00e0.", + "title": "No s'ha pogut afegir el servei" + }, + "user": { + "data": { + "device_input": "Dispositiu" + }, + "description": "Comen\u00e7a introduint el nom del dispositiu (per exemple, cuina o dormitori) o l'adre\u00e7a IP de l'Apple TV que vulguis afegir. Si autom\u00e0ticament es troben dispositius a la teva xarxa, es mostra a continuaci\u00f3. \n\n Si no veus el teu dispositiu o tens problemes, prova d'especificar l'adre\u00e7a IP del dispositiu. \n\n {devices}", + "title": "Configuraci\u00f3 d'una nova Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "No engeguis el dispositiu en iniciar Home Assistant" + }, + "description": "Configuraci\u00f3 dels par\u00e0metres generals del dispositiu" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/cs.json new file mode 100644 index 00000000000..ef392a5a668 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/cs.json @@ -0,0 +1,60 @@ +{ + "config": { + "abort": { + "already_configured_device": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "invalid_config": "Nastaven\u00ed tohoto za\u0159\u00edzen\u00ed je ne\u00fapln\u00e9. Zkuste jej p\u0159idat znovu.", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "error": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "title": "Potvrzen\u00ed p\u0159id\u00e1n\u00ed Apple TV" + }, + "pair_no_pin": { + "description": "Pro slu\u017ebu `{protocol}` je vy\u017eadov\u00e1no p\u00e1rov\u00e1n\u00ed. Pokra\u010dujte zad\u00e1n\u00edm k\u00f3du PIN {pin} na Apple TV.", + "title": "P\u00e1rov\u00e1n\u00ed" + }, + "pair_with_pin": { + "data": { + "pin": "PIN k\u00f3d" + }, + "description": "U protokolu `{protocol}` je vy\u017eadov\u00e1no p\u00e1rov\u00e1n\u00ed. Zadejte pros\u00edm PIN k\u00f3d zobrazen\u00fd na obrazovce. \u00davodn\u00ed nuly mus\u00ed b\u00fdt vynech\u00e1ny, tj. zadejte 123, pokud je zobrazen\u00fd k\u00f3d 0123.", + "title": "P\u00e1rov\u00e1n\u00ed" + }, + "reconfigure": { + "description": "U t\u00e9to Apple TV doch\u00e1z\u00ed k probl\u00e9m\u016fm s p\u0159ipojen\u00edm a je t\u0159eba ji znovu nastavit.", + "title": "Zm\u011bna konfigurace za\u0159\u00edzen\u00ed" + }, + "service_problem": { + "description": "P\u0159i p\u00e1rov\u00e1n\u00ed protokolu `{protocol}` do\u0161lo k probl\u00e9mu. Protokol bude ignorov\u00e1n.", + "title": "Nepoda\u0159ilo se p\u0159idat slu\u017ebu" + }, + "user": { + "data": { + "device_input": "Za\u0159\u00edzen\u00ed" + }, + "description": "Za\u010dn\u011bte zad\u00e1n\u00edm n\u00e1zvu za\u0159\u00edzen\u00ed (nap\u0159. Kuchyn\u011b nebo lo\u017enice) nebo IP adresy Apple TV, kterou chcete p\u0159idat. Pokud byla ve va\u0161\u00ed s\u00edti automaticky nalezena n\u011bkter\u00e1 za\u0159\u00edzen\u00ed, jsou uvedena n\u00ed\u017ee. \n\n Pokud nevid\u00edte sv\u00e9 za\u0159\u00edzen\u00ed nebo nastaly n\u011bjak\u00e9 probl\u00e9my, zkuste zadat IP adresu za\u0159\u00edzen\u00ed. \n\n {devices}", + "title": "Nastaven\u00ed nov\u00e9 Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Nezap\u00ednejte za\u0159\u00edzen\u00ed dokud se Home Assistant spou\u0161t\u00ed" + }, + "description": "Konfigurace obecn\u00fdch mo\u017enost\u00ed za\u0159\u00edzen\u00ed" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/de.json new file mode 100644 index 00000000000..464bad99d5a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/de.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "backoff": "Das Ger\u00e4t akzeptiert derzeit keine Kopplungsanfragen (M\u00f6glicherweise wurde zu oft ein ung\u00fcltiger PIN-Code eingegeben), versuche es sp\u00e4ter erneut.", + "device_did_not_pair": "Es wurde kein Versuch unternommen, den Kopplungsvorgang vom Ger\u00e4t aus abzuschlie\u00dfen.", + "invalid_config": "Die Konfiguration f\u00fcr dieses Ger\u00e4t ist unvollst\u00e4ndig. Bitte versuche, es erneut hinzuzuf\u00fcgen.", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "unknown": "Unerwarteter Fehler" + }, + "error": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "no_usable_service": "Es wurde ein Ger\u00e4t gefunden, aber es konnte keine M\u00f6glichkeit gefunden werden, eine Verbindung zu diesem Ger\u00e4t herzustellen. Wenn diese Meldung weiterhin erscheint, versuche, die IP-Adresse anzugeben oder den Apple TV neu zu starten.", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Es wird der Apple TV mit dem Namen \" {name} \" zu Home Assistant hinzugef\u00fcgt. \n\n ** Um den Vorgang abzuschlie\u00dfen, m\u00fcssen m\u00f6glicherweise mehrere PIN-Codes eingegeben werden. ** \n\n Bitte beachte, dass der Apple TV mit dieser Integration * nicht * ausgeschalten werden kann. Nur der Media Player in Home Assistant wird ausgeschaltet!", + "title": "Best\u00e4tige das Hinzuf\u00fcgen vom Apple TV" + }, + "pair_no_pin": { + "description": "F\u00fcr den Dienst `{protocol}` ist eine Kopplung erforderlich. Bitte gebe die PIN {pin} am Apple TV ein, um fortzufahren.", + "title": "Kopplung" + }, + "pair_with_pin": { + "data": { + "pin": "PIN-Code" + }, + "description": "F\u00fcr das Protokoll `{protocol}` ist eine Kopplung erforderlich. Bitte gebe den auf dem Bildschirm angezeigten PIN-Code ein. F\u00fchrende Nullen m\u00fcssen weggelassen werden, d.h. gebe 123 ein, wenn der angezeigte Code 0123 lautet.", + "title": "Kopplung" + }, + "reconfigure": { + "description": "Dieser Apple TV hat Verbindungsprobleme und muss neu konfiguriert werden.", + "title": "Ger\u00e4teneukonfiguration" + }, + "service_problem": { + "description": "Beim Koppeln des Protokolls `{protocol}` ist ein Problem aufgetreten. Es wird ignoriert.", + "title": "Fehler beim Hinzuf\u00fcgen des Dienstes" + }, + "user": { + "data": { + "device_input": "Ger\u00e4t" + }, + "description": "Gebe zun\u00e4chst den Ger\u00e4tenamen (z. B. K\u00fcche oder Schlafzimmer) oder die IP-Adresse des Apple TV ein, der hinzugef\u00fcgt werden soll. Wenn Ger\u00e4te automatisch im Netzwerk gefunden wurden, werden sie unten angezeigt. \n\nWenn das Ger\u00e4t nicht sichtbar ist oder Probleme auftreten, gebe die IP-Adresse des Ger\u00e4ts an. \n\n{devices}", + "title": "Neuen Apple TV einrichten" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Schalte das Ger\u00e4t nicht ein, wenn Home Assistant startet" + }, + "description": "Konfiguriere die allgemeinen Ger\u00e4teeinstellungen" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/en.json new file mode 100644 index 00000000000..304a43363a0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/en.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "backoff": "Device does not accept pairing reqests at this time (you might have entered an invalid PIN code too many times), try again later.", + "device_did_not_pair": "No attempt to finish pairing process was made from the device.", + "invalid_config": "The configuration for this device is incomplete. Please try adding it again.", + "no_devices_found": "No devices found on the network", + "unknown": "Unexpected error" + }, + "error": { + "already_configured": "Device is already configured", + "invalid_auth": "Invalid authentication", + "no_devices_found": "No devices found on the network", + "no_usable_service": "A device was found but could not identify any way to establish a connection to it. If you keep seeing this message, try specifying its IP address or restarting your Apple TV.", + "unknown": "Unexpected error" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "You are about to add the Apple TV named `{name}` to Home Assistant.\n\n**To complete the process, you may have to enter multiple PIN codes.**\n\nPlease note that you will *not* be able to power off your Apple TV with this integration. Only the media player in Home Assistant will turn off!", + "title": "Confirm adding Apple TV" + }, + "pair_no_pin": { + "description": "Pairing is required for the `{protocol}` service. Please enter PIN {pin} on your Apple TV to continue.", + "title": "Pairing" + }, + "pair_with_pin": { + "data": { + "pin": "PIN Code" + }, + "description": "Pairing is required for the `{protocol}` protocol. Please enter the PIN code displayed on screen. Leading zeros shall be omitted, i.e. enter 123 if the displayed code is 0123.", + "title": "Pairing" + }, + "reconfigure": { + "description": "This Apple TV is experiencing some connection difficulties and must be reconfigured.", + "title": "Device reconfiguration" + }, + "service_problem": { + "description": "A problem occurred while pairing protocol `{protocol}`. It will be ignored.", + "title": "Failed to add service" + }, + "user": { + "data": { + "device_input": "Device" + }, + "description": "Start by entering the device name (e.g. Kitchen or Bedroom) or IP address of the Apple TV you want to add. If any devices were automatically found on your network, they are shown below.\n\nIf you cannot see your device or experience any issues, try specifying the device IP address.\n\n{devices}", + "title": "Setup a new Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Do not turn device on when starting Home Assistant" + }, + "description": "Configure general device settings" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/es.json new file mode 100644 index 00000000000..d03a77ca1c2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/es.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "backoff": "El dispositivo no acepta solicitudes de emparejamiento en este momento (es posible que hayas introducido un c\u00f3digo PIN no v\u00e1lido demasiadas veces), int\u00e9ntalo de nuevo m\u00e1s tarde.", + "device_did_not_pair": "No se ha intentado finalizar el proceso de emparejamiento desde el dispositivo.", + "invalid_config": "La configuraci\u00f3n para este dispositivo est\u00e1 incompleta. Intenta a\u00f1adirlo de nuevo.", + "no_devices_found": "No se encontraron dispositivos en la red", + "unknown": "Error inesperado" + }, + "error": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "no_devices_found": "No se encontraron dispositivos en la red", + "no_usable_service": "Se encontr\u00f3 un dispositivo, pero no se pudo identificar ninguna manera de establecer una conexi\u00f3n con \u00e9l. Si sigues viendo este mensaje, intenta especificar su direcci\u00f3n IP o reiniciar el Apple TV.", + "unknown": "Error inesperado" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Est\u00e1s a punto de a\u00f1adir el Apple TV con nombre `{name}` a Home Assistant.\n\n**Para completar el proceso, puede que tengas que introducir varios c\u00f3digos PIN.**\n\nTen en cuenta que *no* podr\u00e1s apagar tu Apple TV con esta integraci\u00f3n. \u00a1S\u00f3lo se apagar\u00e1 el reproductor de medios de Home Assistant!", + "title": "Confirma la adici\u00f3n del Apple TV" + }, + "pair_no_pin": { + "description": "El emparejamiento es necesario para el servicio `{protocol}`. Introduce el PIN en tu Apple TV para continuar.", + "title": "Emparejamiento" + }, + "pair_with_pin": { + "data": { + "pin": "C\u00f3digo PIN" + }, + "description": "El emparejamiento es necesario para el protocolo `{protocol}`. Introduce el c\u00f3digo PIN que aparece en la pantalla. Los ceros iniciales deben ser omitidos, es decir, introduce 123 si el c\u00f3digo mostrado es 0123.", + "title": "Emparejamiento" + }, + "reconfigure": { + "description": "Este Apple TV est\u00e1 experimentando algunos problemas de conexi\u00f3n y debe ser reconfigurado.", + "title": "Reconfiguraci\u00f3n del dispositivo" + }, + "service_problem": { + "description": "Se ha producido un problema durante el protocolo de emparejamiento `{protocol}`. Ser\u00e1 ignorado.", + "title": "Error al a\u00f1adir el servicio" + }, + "user": { + "data": { + "device_input": "Dispositivo" + }, + "description": "Empieza introduciendo el nombre del dispositivo (eje. Cocina o Dormitorio) o la direcci\u00f3n IP del Apple TV que quieres a\u00f1adir. Si se han econtrado dispositivos en tu red, se mostrar\u00e1n a continuaci\u00f3n.\n\nSi no puedes ver el dispositivo o experimentas alg\u00fan problema, intente especificar la direcci\u00f3n IP del dispositivo.\n\n{devices}", + "title": "Configurar un nuevo Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "No encender el dispositivo al iniciar Home Assistant" + }, + "description": "Configurar los ajustes generales del dispositivo" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/et.json new file mode 100644 index 00000000000..a4a06d8e1b1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/et.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "backoff": "Seade ei aktsepteeri praegu sidumisn\u00f5udeid (v\u00f5ib-olla oled liiga palju kordi vale PIN-koodi sisestanud), proovi hiljem uuesti.", + "device_did_not_pair": "Seade ei \u00fcritatud sidumisprotsessi l\u00f5pule viia.", + "invalid_config": "Selle seadme s\u00e4tted on puudulikud. Proovi see uuesti lisada.", + "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet", + "unknown": "Ootamatu t\u00f5rge" + }, + "error": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "invalid_auth": "Vigane autentimine", + "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet", + "no_usable_service": "Leiti seade kuid ei suudetud tuvastada moodust \u00fchenduse loomiseks. Kui n\u00e4ed seda teadet pidevalt, proovi m\u00e4\u00e4rata seadme IP-aadress v\u00f5i taask\u00e4ivita Apple TV.", + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Oled Home Assistantile lisamas Apple TV-d nimega {name}.\n\n**Protsessi l\u00f5puleviimiseks pead v\u00f5ib-olla sisestama mitu PIN-koodi.**\n\nPane t\u00e4hele, et selle sidumisega * ei saa * v\u00e4lja l\u00fclitada oma Apple TV-d. Ainult Home Assistant-i meediam\u00e4ngija l\u00fclitub v\u00e4lja!", + "title": "Kinnita Apple TV lisamine" + }, + "pair_no_pin": { + "description": "Teenuse {protocol} sidumine on vajalik. J\u00e4tkamiseks sisesta oma Apple TV-s PIN-kood {pin} .", + "title": "Sidumine" + }, + "pair_with_pin": { + "data": { + "pin": "PIN kood" + }, + "description": "Vajalik on protokolli {protocol} sidumine. Sisesta ekraanil kuvatav PIN-kood. Alguse nullid j\u00e4etakse v\u00e4lja, st. sisesta 123, kui kuvatav kood on 0123.", + "title": "Sidumine" + }, + "reconfigure": { + "description": "Sellel Apple TV-l on \u00fchendusprobleemid ja see tuleb uuesti seadistada.", + "title": "Seadme \u00fcmberseadistamine" + }, + "service_problem": { + "description": "Protokolli {protocol} sidumisel ilmnes probleem. Seda ignoreeritakse.", + "title": "Teenuse lisamine eba\u00f5nnestus." + }, + "user": { + "data": { + "device_input": "Seade" + }, + "description": "Alustuseks sisesta lisatava Apple TV seadme nimi (nt K\u00f6\u00f6k v\u00f5i Magamistuba) v\u00f5i IP-aadress. Kui m\u00f5ni seade leiti teie v\u00f5rgust automaatselt kuvatakse see allpool. \n\n Kui ei n\u00e4e oma seadet v\u00f5i on probleeme, proovi m\u00e4\u00e4rata seadme IP-aadress. \n\n {devices}", + "title": "Seadista uus Apple TV sidumine" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "\u00c4ra l\u00fclita seadet Home Assistanti k\u00e4ivitamisel sisse" + }, + "description": "Seadme \u00fclds\u00e4tete seadistamine" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/fr.json new file mode 100644 index 00000000000..e1a719b31c9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/fr.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "Le p\u00e9riph\u00e9rique est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "backoff": "L'appareil n'accepte pas les demandes d'appariement pour le moment (vous avez peut-\u00eatre saisi un code PIN non valide trop de fois), r\u00e9essayez plus tard.", + "device_did_not_pair": "Aucune tentative pour terminer l'appairage n'a \u00e9t\u00e9 effectu\u00e9e \u00e0 partir de l'appareil.", + "invalid_config": "La configuration de cet appareil est incompl\u00e8te. Veuillez r\u00e9essayer de l'ajouter.", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "unknown": "Erreur inattendue" + }, + "error": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "invalid_auth": "Autentification invalide", + "no_devices_found": "Aucun appareil d\u00e9tect\u00e9 sur le r\u00e9seau", + "no_usable_service": "Un dispositif a \u00e9t\u00e9 trouv\u00e9, mais aucun moyen d\u2019\u00e9tablir un lien avec lui. Si vous continuez \u00e0 voir ce message, essayez de sp\u00e9cifier son adresse IP ou de red\u00e9marrer votre Apple TV.", + "unknown": "Erreur innatendue" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Vous \u00eates sur le point d'ajouter l'Apple TV nomm\u00e9e \u00ab {name} \u00bb \u00e0 Home Assistant. \n\n **Pour terminer le processus, vous devrez peut-\u00eatre saisir plusieurs codes PIN.** \n\n Veuillez noter que vous ne pourrez *pas* \u00e9teindre votre Apple TV avec cette int\u00e9gration. Seul le lecteur multim\u00e9dia de Home Assistant s'\u00e9teint!", + "title": "Confirmer l'ajout d'Apple TV" + }, + "pair_no_pin": { + "description": "L'appairage est requis pour le service ` {protocol} `. Veuillez saisir le code PIN {pin} sur votre Apple TV pour continuer.", + "title": "Appairage" + }, + "pair_with_pin": { + "data": { + "pin": "Code PIN" + }, + "description": "L'appairage est requis pour le protocole `{protocol}`. Veuillez saisir le code PIN affich\u00e9 \u00e0 l'\u00e9cran. Les z\u00e9ros doivent \u00eatre omis, c'est-\u00e0-dire entrer 123 si le code affich\u00e9 est 0123.", + "title": "Appairage" + }, + "reconfigure": { + "description": "Cette Apple TV rencontre des difficult\u00e9s de connexion et doit \u00eatre reconfigur\u00e9e.", + "title": "Reconfiguration de l'appareil" + }, + "service_problem": { + "description": "Un probl\u00e8me est survenu lors du couplage du protocole \u00ab {protocol} \u00bb. Il sera ignor\u00e9.", + "title": "\u00c9chec de l'ajout du service" + }, + "user": { + "data": { + "device_input": "Appareil" + }, + "description": "Commencez par entrer le nom de l'appareil (par exemple, Cuisine ou Chambre) ou l'adresse IP de l'Apple TV que vous souhaitez ajouter. Si des appareils ont \u00e9t\u00e9 d\u00e9tect\u00e9s automatiquement sur votre r\u00e9seau, ils sont affich\u00e9s ci-dessous. \n\n Si vous ne voyez pas votre appareil ou rencontrez des probl\u00e8mes, essayez de sp\u00e9cifier l'adresse IP de l'appareil. \n\n {devices}", + "title": "Configurer une nouvelle Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "N'allumez pas l'appareil lors du d\u00e9marrage de Home Assistant" + }, + "description": "Configurer les param\u00e8tres g\u00e9n\u00e9raux de l'appareil" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/hu.json new file mode 100644 index 00000000000..63bf29a73f1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/hu.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured_device": "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", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "error": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "title": "Apple TV sikeresen hozz\u00e1adva" + }, + "pair_no_pin": { + "title": "P\u00e1ros\u00edt\u00e1s" + }, + "pair_with_pin": { + "data": { + "pin": "PIN-k\u00f3d" + }, + "title": "P\u00e1ros\u00edt\u00e1s" + }, + "reconfigure": { + "title": "Eszk\u00f6z \u00fajrakonfigur\u00e1l\u00e1sa" + }, + "service_problem": { + "title": "Nem siker\u00fclt hozz\u00e1adni a szolg\u00e1ltat\u00e1st" + }, + "user": { + "data": { + "device_input": "Eszk\u00f6z" + }, + "title": "\u00daj Apple TV be\u00e1ll\u00edt\u00e1sa" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/id.json new file mode 100644 index 00000000000..5646b498242 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/id.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "backoff": "Perangkat tidak bisa menerima permintaan pemasangan saat ini (Anda mungkin telah berulang kali memasukkan kode PIN yang salah). Coba lagi nanti.", + "device_did_not_pair": "Tidak ada upaya untuk menyelesaikan proses pemasangan dari sisi perangkat.", + "invalid_config": "Konfigurasi untuk perangkat ini tidak lengkap. Coba tambahkan lagi.", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "already_configured": "Perangkat sudah dikonfigurasi", + "invalid_auth": "Autentikasi tidak valid", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "no_usable_service": "Perangkat ditemukan tetapi kami tidak dapat mengidentifikasi berbagai cara untuk membuat koneksi ke perangkat tersebut. Jika Anda terus melihat pesan ini, coba tentukan alamat IP-nya atau mulai ulang Apple TV Anda.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Anda akan menambahkan Apple TV bernama `{name}` ke Home Assistant.\n\n** Untuk menyelesaikan proses, Anda mungkin harus memasukkan kode PIN beberapa kali.**\n\nPerhatikan bahwa Anda *tidak* akan dapat mematikan Apple TV dengan integrasi ini. Hanya pemutar media di Home Assistant yang akan dimatikan!", + "title": "Konfirmasikan menambahkan Apple TV" + }, + "pair_no_pin": { + "description": "Pemasangan diperlukan untuk layanan `{protocol}`. Masukkan PIN {pin} di Apple TV untuk melanjutkan.", + "title": "Memasangkan" + }, + "pair_with_pin": { + "data": { + "pin": "Kode PIN" + }, + "description": "Pemasangan diperlukan untuk protokol `{protocol}`. Masukkan kode PIN yang ditampilkan pada layar. Angka nol di awal harus dihilangkan. Misalnya, masukkan 123 jika kode yang ditampilkan adalah 0123.", + "title": "Memasangkan" + }, + "reconfigure": { + "description": "Apple TV ini mengalami masalah koneksi dan harus dikonfigurasi ulang.", + "title": "Konfigurasi ulang perangkat" + }, + "service_problem": { + "description": "Terjadi masalah saat protokol pemasangan `{protocol}`. Masalah ini akan diabaikan.", + "title": "Gagal menambahkan layanan" + }, + "user": { + "data": { + "device_input": "Perangkat" + }, + "description": "Mulai dengan memasukkan nama perangkat (misalnya Dapur atau Kamar Tidur) atau alamat IP Apple TV yang ingin ditambahkan. Jika ada perangkat yang ditemukan secara otomatis di jaringan Anda, perangkat tersebut akan ditampilkan di bawah ini.\n\nJika Anda tidak dapat melihat perangkat atau mengalami masalah, coba tentukan alamat IP perangkat.\n\n{devices}", + "title": "Siapkan Apple TV baru" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Jangan nyalakan perangkat saat memulai Home Assistant" + }, + "description": "Konfigurasikan pengaturan umum perangkat" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/it.json new file mode 100644 index 00000000000..8faf1be4326 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/it.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "backoff": "Il dispositivo non accetta richieste di abbinamento in questo momento (potresti aver inserito un codice PIN non valido troppe volte), riprova pi\u00f9 tardi.", + "device_did_not_pair": "Nessun tentativo di completare il processo di abbinamento \u00e8 stato effettuato dal dispositivo.", + "invalid_config": "La configurazione per questo dispositivo \u00e8 incompleta. Prova ad aggiungerlo di nuovo.", + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "unknown": "Errore imprevisto" + }, + "error": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "invalid_auth": "Autenticazione non valida", + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "no_usable_service": "\u00c8 stato trovato un dispositivo ma non \u00e8 stato possibile identificare alcun modo per stabilire una connessione ad esso. Se continui a vedere questo messaggio, prova a specificarne l'indirizzo IP o a riavviare l'Apple TV.", + "unknown": "Errore imprevisto" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Stai per aggiungere l'Apple TV denominata \"{name}\" a Home Assistant. \n\n **Per completare la procedura, potrebbe essere necessario inserire pi\u00f9 codici PIN.** \n\nTieni presente che *non* sarai in grado di spegnere la tua Apple TV con questa integrazione. Solo il lettore multimediale in Home Assistant si spegner\u00e0!", + "title": "Conferma l'aggiunta di Apple TV" + }, + "pair_no_pin": { + "description": "L'abbinamento \u00e8 richiesto per il servizio \"{protocol}\". Inserisci il PIN {pin} sulla tua Apple TV per continuare.", + "title": "Abbinamento" + }, + "pair_with_pin": { + "data": { + "pin": "Codice PIN" + }, + "description": "L'abbinamento \u00e8 richiesto per il protocollo \"{protocol}\". Immettere il codice PIN visualizzato sullo schermo. Gli zeri iniziali devono essere omessi, ovvero immettere 123 se il codice visualizzato \u00e8 0123.", + "title": "Abbinamento" + }, + "reconfigure": { + "description": "Questa Apple TV sta riscontrando alcune difficolt\u00e0 di connessione e deve essere riconfigurata.", + "title": "Riconfigurazione del dispositivo" + }, + "service_problem": { + "description": "Si \u00e8 verificato un problema durante l'associazione del protocollo \"{protocol}\". Sar\u00e0 ignorato.", + "title": "Impossibile aggiungere il servizio" + }, + "user": { + "data": { + "device_input": "Dispositivo" + }, + "description": "Inizia inserendo il nome del dispositivo (es. Cucina o Camera da letto) o l'indirizzo IP dell'Apple TV che desideri aggiungere. Se sono stati rilevati automaticamente dei dispositivi sulla rete, verranno visualizzati di seguito. \n\n Se non riesci a vedere il tuo dispositivo o riscontri problemi, prova a specificare l'indirizzo IP del dispositivo. \n\n {devices}", + "title": "Configura una nuova Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Non accendere il dispositivo all'avvio di Home Assistant" + }, + "description": "Configurare le impostazioni generali del dispositivo" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/ko.json new file mode 100644 index 00000000000..278dbec04e4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/ko.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "backoff": "\uae30\uae30\uac00 \ud604\uc7ac \ud398\uc5b4\ub9c1 \uc694\uccad\uc744 \uc218\ub77d\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4(\uc798\ubabb\ub41c PIN \ucf54\ub4dc\ub97c \ub108\ubb34 \ub9ce\uc774 \uc785\ub825\ud588\uc744 \uc218 \uc788\uc74c). \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "device_did_not_pair": "\uae30\uae30\uc5d0\uc11c \ud398\uc5b4\ub9c1 \ud504\ub85c\uc138\uc2a4\ub97c \uc644\ub8cc\ud558\ub824\uace0 \uc2dc\ub3c4\ud558\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "invalid_config": "\uc774 \uae30\uae30\uc5d0 \ub300\ud55c \uad6c\uc131\uc774 \ubd88\uc644\uc804\ud569\ub2c8\ub2e4. \ub2e4\uc2dc \ucd94\uac00\ud574\uc8fc\uc138\uc694.", + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "no_usable_service": "\uae30\uae30\ub97c \ucc3e\uc558\uc9c0\ub9cc \uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\ub294 \ubc29\ubc95\uc744 \uc2dd\ubcc4\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uba54\uc2dc\uc9c0\uac00 \uacc4\uc18d \ud45c\uc2dc\ub418\uba74 \ud574\ub2f9 IP \uc8fc\uc18c\ub97c \uc9c1\uc811 \uc9c0\uc815\ud574\uc8fc\uc2dc\uac70\ub098 Apple TV\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud574\uc8fc\uc138\uc694.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Apple TV `{name}`\uc744(\ub97c) Home Assistant\uc5d0 \ucd94\uac00\ud558\ub824\uace0 \ud569\ub2c8\ub2e4.\n\n**\ud504\ub85c\uc138\uc2a4\ub97c \uc644\ub8cc\ud558\ub824\uba74 \uc5ec\ub7ec \uac1c\uc758 PIN \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc57c \ud560 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4.**\n\n\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \ud1b5\ud574 Apple TV\uc758 \uc804\uc6d0\uc740 *\ub04c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4*. Home Assistant\uc758 \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4\ub9cc \uaebc\uc9d1\ub2c8\ub2e4!", + "title": "Apple TV \ucd94\uac00 \ud655\uc778\ud558\uae30" + }, + "pair_no_pin": { + "description": "`{protocol}` \uc11c\ube44\uc2a4\uc5d0 \ub300\ud55c \ud398\uc5b4\ub9c1\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \uacc4\uc18d\ud558\ub824\uba74 Apple TV\uc5d0 PIN {pin}\uc744(\ub97c) \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "\ud398\uc5b4\ub9c1\ud558\uae30" + }, + "pair_with_pin": { + "data": { + "pin": "PIN \ucf54\ub4dc" + }, + "description": "`{protocol}` \ud504\ub85c\ud1a0\ucf5c\uc5d0 \ub300\ud55c \ud398\uc5b4\ub9c1\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \ud654\uba74\uc5d0 \ud45c\uc2dc\ub418\ub294 PIN \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc55e\uc790\ub9ac\uc758 0\uc740 \uc0dd\ub7b5\ub418\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc989, \ud45c\uc2dc\ub41c \ucf54\ub4dc\uac00 0123\uc774\uba74 123\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "\ud398\uc5b4\ub9c1\ud558\uae30" + }, + "reconfigure": { + "description": "\uc774 Apple TV\uc5d0 \uc5f0\uacb0 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud558\uc5ec \ub2e4\uc2dc \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4.", + "title": "\uae30\uae30 \uc7ac\uad6c\uc131" + }, + "service_problem": { + "description": "\ud504\ub85c\ud1a0\ucf5c `{protocol}`\uc744(\ub97c) \ud398\uc5b4\ub9c1\ud558\ub294 \ub3d9\uc548 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\ub294 \ubb34\uc2dc\ub429\ub2c8\ub2e4.", + "title": "\uc11c\ube44\uc2a4\ub97c \ucd94\uac00\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "user": { + "data": { + "device_input": "\uae30\uae30" + }, + "description": "\uba3c\uc800 \ucd94\uac00\ud560 Apple TV\uc758 \uae30\uae30 \uc774\ub984(\uc608: \uc8fc\ubc29 \ub610\ub294 \uce68\uc2e4) \ub610\ub294 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uc7a5\uce58\uac00 \uc790\ub3d9\uc73c\ub85c \ubc1c\uacac\ub41c \uacbd\uc6b0 \ub2e4\uc74c\uacfc \uac19\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4.\n\n\uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uac70\ub098 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud55c \uacbd\uc6b0 \uae30\uae30 IP \uc8fc\uc18c\ub97c \uc9c1\uc811 \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\n{devices}", + "title": "\uc0c8\ub85c\uc6b4 Apple TV \uc124\uc815\ud558\uae30" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Home Assistant\ub97c \uc2dc\uc791\ud560 \ub54c \uae30\uae30\ub97c \ucf1c\uc9c0 \ub9d0\uc544 \uc8fc\uc138\uc694" + }, + "description": "\uc77c\ubc18 \uae30\uae30 \uc124\uc815 \uad6c\uc131" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/lb.json new file mode 100644 index 00000000000..2354033b577 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/lb.json @@ -0,0 +1,60 @@ +{ + "config": { + "abort": { + "already_configured_device": "Apparat ass scho konfigur\u00e9iert", + "already_in_progress": "Konfiguratioun's Oflaf ass schon am gaang", + "invalid_config": "Konfiguratioun fir d\u00ebsen Apparat ass net komplett. Prob\u00e9ier fir et nach emol dob\u00e4i ze setzen.", + "no_devices_found": "Keng Apparater am Netzwierk fonnt", + "unknown": "Onerwaarte Feeler" + }, + "error": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "no_devices_found": "Keng Apparater am Netzwierk fonnt", + "unknown": "Onerwaarte Feeler" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Du bass um Punkt fir den Apple TV mam Numm \"{name}\" am Home Assistant dob\u00e4izesetzen.\n\n**Fir de Prozess ofzeschl\u00e9issen, muss Du vill\u00e4icht m\u00e9i PIN-Coden aginn.**\n\nNot\u00e9ier w.e.g dass Du d\u00e4in Apple TV mat d\u00ebser Integratioun *net\" ausschalten kanns. N\u00ebmmen de Mediaspiller am Home Assistant schalt aus!", + "title": "Apple TV dob\u00e4isetzen best\u00e4tegen" + }, + "pair_no_pin": { + "description": "Kopplung ass n\u00e9ideg fir de `{protocol}` Service. G\u00ebff de PIN {pin} op dengem Apple TV an fir w\u00e9iderzefueren", + "title": "Kopplung" + }, + "pair_with_pin": { + "data": { + "pin": "PIN Code" + }, + "description": "Kopplung ass n\u00e9ideg fir de `{protocol}` Protokoll. G\u00ebff de PIN code un deen um Ecran ugewise g\u00ebtt. Nullen op der 1ter Plaatz ginn ewechgelooss, dh g\u00ebff 123 wann de gewise Code 0123 ass.", + "title": "Kopplung" + }, + "reconfigure": { + "description": "D\u00ebsen Apple TV huet e puer Verbindungsschwieregkeeten a muss nei konfigur\u00e9iert ginn.", + "title": "Apparat Rekonfiguratioun" + }, + "service_problem": { + "title": "Feeler beim dob\u00e4isetze vum Service" + }, + "user": { + "data": { + "device_input": "Apparat" + }, + "description": "F\u00e4nk un andeems Du den Numm vum Apparat (z. B. Kichen oder Schlofkummer) oder IP Adress vum Apple TV deen soll dob\u00e4igesat ginn ag\u00ebss.\n\nFalls d\u00e4in Apparat nez ugewise g\u00ebtt oder iergendwelch Problemer hues, prob\u00e9ier d'IP Adress vum Apparat anzeginn.\n\n{devices}", + "title": "Neien Apple TV ariichten" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Schlalt den Apparat net un wann den Home Assistant start" + }, + "description": "Allgemeng Apparat Astellungen konfigur\u00e9ieren" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/nl.json new file mode 100644 index 00000000000..cc04522334d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/nl.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", + "backoff": "Het apparaat accepteert op dit moment geen koppelingsverzoeken (u heeft mogelijk te vaak een ongeldige pincode ingevoerd), probeer het later opnieuw.", + "device_did_not_pair": "Er is geen poging gedaan om het koppelingsproces te voltooien vanaf het apparaat.", + "invalid_config": "De configuratie voor dit apparaat is onvolledig. Probeer het opnieuw toe te voegen.", + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "unknown": "Onverwachte fout" + }, + "error": { + "already_configured": "Apparaat is al geconfigureerd", + "invalid_auth": "Ongeldige authenticatie", + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "no_usable_service": "Er is een apparaat gevonden, maar er kon geen manier worden gevonden om er verbinding mee te maken. Als u dit bericht blijft zien, probeert u het IP-adres in te voeren of uw Apple TV opnieuw op te starten.", + "unknown": "Onverwachte fout" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "U staat op het punt om de Apple TV met de naam `{name}` toe te voegen aan Home Assistant.\n\n**Om het proces te voltooien, moet u mogelijk meerdere PIN-codes invoeren.**\n\nLet op: u kunt uw Apple TV *niet* uitschakelen met deze integratie. Alleen de mediaspeler in Home Assistant wordt uitgeschakeld!", + "title": "Bevestig het toevoegen van Apple TV" + }, + "pair_no_pin": { + "description": "Koppeling is vereist voor de `{protocol}` service. Voer de PIN {pin} in op uw Apple TV om verder te gaan.", + "title": "Koppelen" + }, + "pair_with_pin": { + "data": { + "pin": "PIN-code" + }, + "description": "Koppelen is vereist voor het `{protocol}` protocol. Voer de PIN-code in die op het scherm wordt getoond. Beginnende nullen moeten worden weggelaten, d.w.z. voer 123 in als de getoonde code 0123 is.", + "title": "Koppelen" + }, + "reconfigure": { + "description": "Deze Apple TV ondervindt verbindingsproblemen en moet opnieuw worden geconfigureerd.", + "title": "Apparaat herconfiguratie" + }, + "service_problem": { + "description": "Er is een probleem opgetreden tijdens het koppelen van protocol `{protocol}`. Dit wordt genegeerd.", + "title": "Dienst toevoegen mislukt" + }, + "user": { + "data": { + "device_input": "Apparaat" + }, + "description": "Begin met het invoeren van de apparaatnaam (bijv. Keuken of Slaapkamer) of het IP-adres van de Apple TV die u wilt toevoegen. Als er automatisch apparaten in uw netwerk zijn gevonden, worden deze hieronder weergegeven.\n\nAls u het apparaat niet kunt zien of problemen ondervindt, probeer dan het IP-adres van het apparaat in te voeren.\n\n{devices}", + "title": "Stel een nieuwe Apple TV in" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Schakel het apparaat niet in wanneer u Home Assistant start" + }, + "description": "Algemene apparaatinstellingen configureren" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/no.json new file mode 100644 index 00000000000..993f7708367 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/no.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "backoff": "Enheten godtar ikke parringsanmodninger for \u00f8yeblikket (du har kanskje angitt en ugyldig PIN-kode for mange ganger), pr\u00f8v igjen senere.", + "device_did_not_pair": "Ingen fors\u00f8k p\u00e5 \u00e5 fullf\u00f8re paringsprosessen ble gjort fra enheten", + "invalid_config": "Konfigurasjonen for denne enheten er ufullstendig. Pr\u00f8v \u00e5 legge den til p\u00e5 nytt.", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "unknown": "Uventet feil" + }, + "error": { + "already_configured": "Enheten er allerede konfigurert", + "invalid_auth": "Ugyldig godkjenning", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "no_usable_service": "En enhet ble funnet, men kunne ikke identifisere noen m\u00e5te \u00e5 etablere en tilkobling til den. Hvis du fortsetter \u00e5 se denne meldingen, kan du pr\u00f8ve \u00e5 angi IP-adressen eller starte Apple TV p\u00e5 nytt.", + "unknown": "Uventet feil" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Du er i ferd med \u00e5 legge til Apple TV med navnet {name} i Home Assistant.\n\n**For \u00e5 fullf\u00f8re prosessen m\u00e5 du kanskje angi flere PIN-koder.**\n\nV\u00e6r oppmerksom p\u00e5 at du *ikke* kan sl\u00e5 av Apple TV med denne integreringen. Bare mediespilleren i Home Assistant sl\u00e5r seg av!", + "title": "Bekreft at du legger til Apple TV" + }, + "pair_no_pin": { + "description": "Paring kreves for tjenesten {protocol}. Skriv inn PIN-koden {pin} p\u00e5 Apple TV for \u00e5 fortsette.", + "title": "Sammenkobling" + }, + "pair_with_pin": { + "data": { + "pin": "PIN kode" + }, + "description": "Paring kreves for protokollen {protocol}. Skriv inn PIN-koden som vises p\u00e5 skjermen. Ledende nuller utelates, det vil si angi 123 hvis den viste koden er 0123.", + "title": "Sammenkobling" + }, + "reconfigure": { + "description": "Denne Apple TVen har noen tilkoblingsvansker og m\u00e5 konfigureres p\u00e5 nytt", + "title": "Omkonfigurering av enheter" + }, + "service_problem": { + "description": "Det oppstod et problem under sammenkobling av protokollen \"{protocol}\". Det vil bli ignorert.", + "title": "Kunne ikke legge til tjenesten" + }, + "user": { + "data": { + "device_input": "Enhet" + }, + "description": "Start med \u00e5 skrive inn enhetsnavnet (f.eks. kj\u00f8kken eller soverom) eller IP-adressen til Apple TV-en du vil legge til. Hvis noen enheter ble funnet automatisk p\u00e5 nettverket ditt, vises de nedenfor.\n\nHvis du ikke kan se enheten eller oppleve problemer, kan du pr\u00f8ve \u00e5 angi enhetens IP-adresse.\n\n{devices}", + "title": "Konfigurere en ny Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Ikke sl\u00e5 p\u00e5 enheten n\u00e5r du starter Home Assistant" + }, + "description": "Konfigurer generelle enhetsinnstillinger" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/pl.json new file mode 100644 index 00000000000..48de231527e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/pl.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "backoff": "Urz\u0105dzenie w tej chwili nie akceptuje \u017c\u0105da\u0144 parowania (by\u0107 mo\u017ce zbyt wiele razy wpisa\u0142e\u015b nieprawid\u0142owy kod PIN), spr\u00f3buj ponownie p\u00f3\u017aniej.", + "device_did_not_pair": "Nie podj\u0119to pr\u00f3by zako\u0144czenia procesu parowania z urz\u0105dzenia.", + "invalid_config": "Konfiguracja tego urz\u0105dzenia jest niekompletna. Spr\u00f3buj doda\u0107 go ponownie.", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "error": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "no_usable_service": "Znaleziono urz\u0105dzenie, ale nie uda\u0142o si\u0119 zidentyfikowa\u0107 \u017cadnego sposobu na nawi\u0105zanie z nim po\u0142\u0105czenia. Je\u015bli nadal widzisz t\u0119 wiadomo\u015b\u0107, spr\u00f3buj poda\u0107 jego adres IP lub uruchom ponownie Apple TV.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Zamierzasz doda\u0107 Apple TV o nazwie \"{name}\" do Home Assistanta. \n\n **Aby uko\u0144czy\u0107 ca\u0142y proces, mo\u017ce by\u0107 konieczne wprowadzenie wielu kod\u00f3w PIN.** \n\nPami\u0119taj, \u017ce \"NIE\" b\u0119dziesz w stanie wy\u0142\u0105czy\u0107 Apple TV dzi\u0119ki tej integracji. Wy\u0142\u0105cza si\u0119 tylko sam odtwarzacz multimedialny w Home Assistant!", + "title": "Potwierdzenie dodania Apple TV" + }, + "pair_no_pin": { + "description": "Parowanie jest wymagane dla us\u0142ugi \"{protocol}\". Aby kontynuowa\u0107, wprowad\u017a kod {pin} na swoim Apple TV.", + "title": "Parowanie" + }, + "pair_with_pin": { + "data": { + "pin": "Kod PIN" + }, + "description": "Parowanie jest wymagane dla protoko\u0142u \"{protocol}\". Wprowad\u017a kod PIN wy\u015bwietlony na ekranie. Zera poprzedzaj\u0105ce nale\u017cy pomin\u0105\u0107, tj. wpisa\u0107 123, zamiast 0123.", + "title": "Parowanie" + }, + "reconfigure": { + "description": "Ten Apple TV ma pewne problemy z po\u0142\u0105czeniem i musi zosta\u0107 ponownie skonfigurowany.", + "title": "Ponowna konfiguracja urz\u0105dzenia" + }, + "service_problem": { + "description": "Wyst\u0105pi\u0142 problem podczas parowania protoko\u0142u \"{protocol}\". Zostanie on zignorowany.", + "title": "Nie uda\u0142o si\u0119 doda\u0107 us\u0142ugi" + }, + "user": { + "data": { + "device_input": "Urz\u0105dzenie" + }, + "description": "Zacznij od wprowadzenia nazwy urz\u0105dzenia (np. Kuchnia lub Sypialnia) lub adresu IP Apple TV, kt\u00f3re chcesz doda\u0107. Je\u015bli jakie\u015b urz\u0105dzenia zosta\u0142y automatycznie znalezione w Twojej sieci, s\u0105 one pokazane poni\u017cej. \n\nJe\u015bli nie widzisz swojego urz\u0105dzenia lub wyst\u0119puj\u0105 jakiekolwiek problemy, spr\u00f3buj okre\u015bli\u0107 adres IP urz\u0105dzenia. \n\n{devices}", + "title": "Konfiguracja nowego Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Nie w\u0142\u0105czaj urz\u0105dzenia podczas uruchamiania Home Assistanta" + }, + "description": "Skonfiguruj og\u00f3lne ustawienia urz\u0105dzenia" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/pt.json new file mode 100644 index 00000000000..486ff0c51e4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/pt.json @@ -0,0 +1,61 @@ +{ + "config": { + "abort": { + "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "unknown": "Erro inesperado" + }, + "error": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "no_usable_service": "Foi encontrado um dispositivo, mas n\u00e3o foi poss\u00edvel identificar nenhuma forma de estabelecer uma liga\u00e7\u00e3o com ele. Se continuar a ver esta mensagem, tente especificar o endere\u00e7o IP ou reiniciar a sua Apple TV.", + "unknown": "Erro inesperado" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Est\u00e1 prestes a adicionar a Apple TV com o nome `{name}` ao Home Assistant.\n\n** Para completar o processo, poder\u00e1 ter que inserir v\u00e1rios c\u00f3digos PIN.**\n\nNote que *n\u00e3o* conseguir\u00e1 desligar a sua Apple TV com esta integra\u00e7\u00e3o. Apenas o media player no Home Assistant ser\u00e1 desligado!", + "title": "Confirme a adi\u00e7\u00e3o da Apple TV" + }, + "pair_no_pin": { + "description": "\u00c9 necess\u00e1rio fazer o emparelhamento com protocolo `{protocol}`. Insira o c\u00f3digo PIN {pin} na sua Apple TV para continuar.", + "title": "Emparelhamento" + }, + "pair_with_pin": { + "data": { + "pin": "C\u00f3digo PIN" + }, + "description": "\u00c9 necess\u00e1rio fazer o emparelhamento com protocolo `{protocol}`. Insira o c\u00f3digo PIN exibido no ecran. Os zeros iniciais devem ser omitidos, ou seja, digite 123 se o c\u00f3digo exibido for 0123.", + "title": "Emparelhamento" + }, + "reconfigure": { + "description": "Esta Apple TV apresenta dificuldades de liga\u00e7\u00e3o e precisa ser reconfigurada.", + "title": "Reconfigura\u00e7\u00e3o do dispositivo" + }, + "service_problem": { + "description": "Ocorreu um problema durante o protocolo de emparelhamento `{protocol}`. Ser\u00e1 ignorado.", + "title": "Falha ao adicionar servi\u00e7o" + }, + "user": { + "data": { + "device_input": "Dispositivo" + }, + "description": "Comece por introduzir o nome do dispositivo (por exemplo, Cozinha ou Quarto) ou o endere\u00e7o IP da Apple TV que pretende adicionar. Se algum dispositivo foi automaticamente encontrado na sua rede, ele \u00e9 mostrado abaixo.\n\nSe n\u00e3o conseguir ver o seu dispositivo ou se tiver algum problema, tente especificar o endere\u00e7o IP do dispositivo.\n\n{devices}", + "title": "Configure uma nova Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "N\u00e3o ligue o dispositivo ao iniciar o Home Assistant" + }, + "description": "Definir as configura\u00e7\u00f5es gerais do dispositivo" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/ru.json new file mode 100644 index 00000000000..b37452d6bcb --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/ru.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "backoff": "\u0412 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043d\u0430 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 (\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e, \u0412\u044b \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u0440\u0430\u0437 \u0432\u0432\u043e\u0434\u0438\u043b\u0438 \u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434), \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", + "device_did_not_pair": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u044b\u0442\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f.", + "invalid_config": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0435\u0433\u043e \u0435\u0449\u0451 \u0440\u0430\u0437.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "error": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "no_usable_service": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u043e\u043c\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u0415\u0441\u043b\u0438 \u0412\u044b \u0443\u0436\u0435 \u0432\u0438\u0434\u0435\u043b\u0438 \u044d\u0442\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 \u0435\u0433\u043e.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u0412\u044b \u0441\u043e\u0431\u0438\u0440\u0430\u0435\u0442\u0435\u0441\u044c \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c Apple TV `{name}` \u0432 Home Assistant. \n\n**\u0414\u043b\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \u0412\u0430\u043c \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u0442\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432\u0432\u0435\u0441\u0442\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e PIN-\u043a\u043e\u0434\u043e\u0432.** \n\n\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u0412\u044b *\u043d\u0435* \u0441\u043c\u043e\u0436\u0435\u0442\u0435 \u0432\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c Apple TV \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438. \u0412 Home Assistant \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0432\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440!", + "title": "\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 Apple TV" + }, + "pair_no_pin": { + "description": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u0441\u043b\u0443\u0436\u0431\u044b`{protocol}`. \u0414\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434 {pin} \u043d\u0430 \u0412\u0430\u0448\u0435\u043c Apple TV.", + "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435" + }, + "pair_with_pin": { + "data": { + "pin": "PIN-\u043a\u043e\u0434" + }, + "description": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 `{protocol}`. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u043c\u044b\u0439 \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435. \u041f\u0435\u0440\u0432\u044b\u0435 \u043d\u0443\u043b\u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u043e\u043f\u0443\u0449\u0435\u043d\u044b, \u0442.\u0435. \u0432\u0432\u0435\u0434\u0438\u0442\u0435 123, \u0435\u0441\u043b\u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u043c\u044b\u0439 \u043a\u043e\u0434 0123.", + "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435" + }, + "reconfigure": { + "description": "\u0423 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Apple TV \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u044e\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u043f\u0440\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438, \u0435\u0433\u043e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c.", + "title": "\u041f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + }, + "service_problem": { + "description": "\u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u043f\u0440\u0438 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0438 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 `{protocol}`. \u042d\u0442\u043e \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u043e\u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e.", + "title": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0441\u043b\u0443\u0436\u0431\u0443" + }, + "user": { + "data": { + "device_input": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u041d\u0430\u0447\u043d\u0438\u0442\u0435 \u0441 \u0432\u0432\u043e\u0434\u0430 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u041a\u0443\u0445\u043d\u044f \u0438\u043b\u0438 \u0421\u043f\u0430\u043b\u044c\u043d\u044f) \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441\u0430 Apple TV, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c. \u0415\u0441\u043b\u0438 \u043a\u0430\u043a\u0438\u0435-\u043b\u0438\u0431\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0431\u044b\u043b\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b \u0432 \u0412\u0430\u0448\u0435\u0439 \u0441\u0435\u0442\u0438, \u043e\u043d\u0438 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u044b \u043d\u0438\u0436\u0435. \n\n\u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0435 \u0432\u0438\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438\u043b\u0438 \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u044e\u0442 \u043a\u0430\u043a\u0438\u0435-\u043b\u0438\u0431\u043e \u0434\u0440\u0443\u0433\u0438\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u043f\u0440\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \n\n {devices}", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043d\u043e\u0432\u043e\u0433\u043e Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "\u041d\u0435 \u0432\u043a\u043b\u044e\u0447\u0430\u0439\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0435 Home Assistant" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/sl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/sl.json new file mode 100644 index 00000000000..997d60402ca --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/sl.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "Naprava je \u017ee name\u0161\u010dena", + "already_in_progress": "Name\u0161\u010danje se \u017ee izvaja", + "backoff": "Naprav v tem trenutku ne sprejema zahtev za seznanitev (morda ste preve\u010dkrat vnesli napa\u010den PIN). Pokusitve znova kasneje.", + "device_did_not_pair": "Iz te naprave ni bilo poskusov zaklju\u010diti seznanjanja.", + "invalid_config": "Namestitev te naprave ni bila zaklju\u010dena. Poskusite ponovno.", + "no_devices_found": "Ni najdenih naprav v omre\u017eju", + "unknown": "Nepri\u010dakovana napaka" + }, + "error": { + "already_configured": "Naprava je \u017ee name\u0161\u010dena", + "invalid_auth": "Napaka pri overjanju", + "no_devices_found": "Ni najdenih naprav v omre\u017eju", + "no_usable_service": "Najdena je bila naprava, za katero ni znan na\u010din povezovanja. \u010ce boste \u0161e vedno videli to sporo\u010dilo, poskusite dolo\u010diti IP naslov ali pa ponovno za\u017eenite Apple TV.", + "unknown": "Nepri\u010dakovana napaka" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "V Home Assistant nameravate dodati Apple TV z imenom `{name}`.\n\n**Za dokon\u010danje postopka boste morda morali ve\u010dkrat vnesti PIN kodo**\n\nS to integracijo ne boste mogli ugasniti svojega Apple TV. Ugasnjena bosta zgolj medijski predvajalnik in Home Assistant!", + "title": "Potrdite dodajanje Apple TV" + }, + "pair_no_pin": { + "description": "Protokol '{protocol}` zahteva seznanitev. Vnesite PIN {pin}, ki je prikazan na Apple TV.", + "title": "Seznanjanje" + }, + "pair_with_pin": { + "data": { + "pin": "PIN koda" + }, + "description": "Protokol '{protocol}` zahteva seznanitev. Vnesite PIN, ki je prikazan na zaslonu. Vodilnih ni\u010del ne vna\u0161ajte - vnesite 123, \u010de je prikazano 0123.", + "title": "Seznanjanje" + }, + "reconfigure": { + "description": "Ta Apple TV ima nekaj te\u017eav in mora biti ponovno konfiguriran.", + "title": "Ponovna namestitev naprave" + }, + "service_problem": { + "description": "Pri usklajevanju protokola `{protocol}` je pri\u0161lo do te\u017eave. Ta bo prezrta.", + "title": "Naprave ni mogo\u010de dodati" + }, + "user": { + "data": { + "device_input": "Naprava" + }, + "description": "Za\u010dnite z vnosom imena naprave (npr. kuhinja ali splanica) ali IP naslova Apple TV, ki bi ga radi dodali. \u010ce so katere naprave bile najdene samodejno v omre\u017eju, so prikazane spodaj.\n\n\u010ce ne vidite svoje naprave ali imate te\u017eave, poskusite dolo\u010diti nov IP.\n\n{devices}", + "title": "Namesti nov Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Ne vkpaljajte naprave ob zagonu Home Assistant-a" + }, + "description": "Konfiguracija splo\u0161nih nastavitev naprave" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/tr.json new file mode 100644 index 00000000000..f33e3998af6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/tr.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "invalid_config": "Bu ayg\u0131t\u0131n yap\u0131land\u0131rmas\u0131 tamamlanmad\u0131. L\u00fctfen tekrar eklemeyi deneyin.", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "unknown": "Beklenmeyen hata" + }, + "error": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "title": "Apple TV eklemeyi onaylay\u0131n" + }, + "pair_no_pin": { + "title": "E\u015fle\u015ftirme" + }, + "pair_with_pin": { + "data": { + "pin": "PIN Kodu" + }, + "title": "E\u015fle\u015ftirme" + }, + "reconfigure": { + "description": "Bu Apple TV baz\u0131 ba\u011flant\u0131 sorunlar\u0131 ya\u015f\u0131yor ve yeniden yap\u0131land\u0131r\u0131lmas\u0131 gerekiyor.", + "title": "Cihaz\u0131n yeniden yap\u0131land\u0131r\u0131lmas\u0131" + }, + "service_problem": { + "title": "Hizmet eklenemedi" + }, + "user": { + "data": { + "device_input": "Cihaz" + }, + "title": "Yeni bir Apple TV kurun" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Home Assistant'\u0131 ba\u015flat\u0131rken cihaz\u0131 a\u00e7may\u0131n" + }, + "description": "Genel cihaz ayarlar\u0131n\u0131 yap\u0131land\u0131r\u0131n" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/uk.json new file mode 100644 index 00000000000..a1ae2259ada --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/uk.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "backoff": "\u0412 \u0434\u0430\u043d\u0438\u0439 \u0447\u0430\u0441 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0440\u0438\u0439\u043c\u0430\u0454 \u0437\u0430\u043f\u0438\u0442\u0438 \u043d\u0430 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438 (\u043c\u043e\u0436\u043b\u0438\u0432\u043e, \u0412\u0438 \u0437\u0430\u043d\u0430\u0434\u0442\u043e \u0431\u0430\u0433\u0430\u0442\u043e \u0440\u0430\u0437 \u0432\u0432\u043e\u0434\u0438\u043b\u0438 \u043d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 PIN-\u043a\u043e\u0434), \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437 \u043f\u0456\u0437\u043d\u0456\u0448\u0435.", + "device_did_not_pair": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043d\u0430\u043c\u0430\u0433\u0430\u0432\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043f\u0440\u043e\u0446\u0435\u0441 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438.", + "invalid_config": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u0446\u044c\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u043d\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430. \u0421\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u0439\u043e\u0433\u043e \u0449\u0435 \u0440\u0430\u0437.", + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "no_usable_service": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u0432\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438 \u0441\u043f\u043e\u0441\u0456\u0431 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e. \u042f\u043a\u0449\u043e \u0412\u0438 \u0432\u0436\u0435 \u0431\u0430\u0447\u0438\u043b\u0438 \u0446\u0435 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u043d\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0432\u043a\u0430\u0437\u0430\u0442\u0438 IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0430\u0431\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0456\u0442\u044c \u0439\u043e\u0433\u043e.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "\u0412\u0438 \u0437\u0431\u0438\u0440\u0430\u0454\u0442\u0435\u0441\u044f \u0434\u043e\u0434\u0430\u0442\u0438 Apple TV `{name}` \u0432 Home Assistant. \n\n ** \u0414\u043b\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044f \u043f\u0440\u043e\u0446\u0435\u0441\u0443 \u0412\u0430\u043c \u043c\u043e\u0436\u0435 \u0437\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0438\u0441\u044f \u0432\u0432\u0435\u0441\u0442\u0438 \u043a\u0456\u043b\u044c\u043a\u0430 PIN-\u043a\u043e\u0434\u0456\u0432. ** \n\n\u0417\u0432\u0435\u0440\u043d\u0456\u0442\u044c \u0443\u0432\u0430\u0433\u0443, \u0449\u043e \u0412\u0438 *\u043d\u0435* \u0437\u043c\u043e\u0436\u0435\u0442\u0435 \u0432\u0438\u043c\u0438\u043a\u0430\u0442\u0438 Apple TV \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0446\u0456\u0454\u0457 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457. \u0412 Home Assistant \u043c\u043e\u0436\u043b\u0438\u0432\u043e \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438 \u0442\u0456\u043b\u044c\u043a\u0438 \u043c\u0435\u0434\u0456\u0430\u043f\u0440\u043e\u0433\u0440\u0430\u0432\u0430\u0447!", + "title": "\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0456\u0442\u044c \u0434\u043e\u0434\u0430\u0432\u0430\u043d\u043d\u044f Apple TV" + }, + "pair_no_pin": { + "description": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0434\u043b\u044f \u0441\u043b\u0443\u0436\u0431\u0438 `{protocol}`. \u0414\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u0432\u0436\u0435\u043d\u043d\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0432\u0435\u0434\u0456\u0442\u044c PIN-\u043a\u043e\u0434 {pin} \u043d\u0430 \u0412\u0430\u0448\u043e\u043c\u0443 Apple TV.", + "title": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "pair_with_pin": { + "data": { + "pin": "PIN-\u043a\u043e\u0434" + }, + "description": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 `{protocol}`. \u0412\u0432\u0435\u0434\u0456\u0442\u044c PIN-\u043a\u043e\u0434, \u044f\u043a\u0438\u0439 \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0454\u0442\u044c\u0441\u044f \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456. \u041f\u0435\u0440\u0448\u0456 \u043d\u0443\u043b\u0456 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0431\u0443\u0442\u0438 \u043e\u043f\u0443\u0449\u0435\u043d\u0456, \u0442\u043e\u0431\u0442\u043e \u0432\u0432\u0435\u0434\u0456\u0442\u044c 123, \u044f\u043a\u0449\u043e \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0454\u0442\u044c\u0441\u044f \u043a\u043e\u0434 0123.", + "title": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "reconfigure": { + "description": "\u0423 \u0446\u044c\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Apple TV \u0432\u0438\u043d\u0438\u043a\u0430\u044e\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u043f\u0440\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u0456, \u0439\u043e\u0433\u043e \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u043f\u0435\u0440\u0435\u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438.", + "title": "\u041f\u0435\u0440\u0435\u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "service_problem": { + "description": "\u0412\u0438\u043d\u0438\u043a\u043b\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u043f\u0440\u0438 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u0456 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 `{protocol}`. \u0426\u0435 \u0431\u0443\u0434\u0435 \u043f\u0440\u043e\u0456\u0433\u043d\u043e\u0440\u043e\u0432\u0430\u043d\u043e.", + "title": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0434\u043e\u0434\u0430\u0442\u0438 \u0441\u043b\u0443\u0436\u0431\u0443" + }, + "user": { + "data": { + "device_input": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + }, + "description": "\u041f\u043e\u0447\u043d\u0456\u0442\u044c \u0437 \u0432\u0432\u0435\u0434\u0435\u043d\u043d\u044f \u043d\u0430\u0437\u0432\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434, \u041a\u0443\u0445\u043d\u044f \u0430\u0431\u043e \u0421\u043f\u0430\u043b\u044c\u043d\u044f) \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0438 Apple TV, \u044f\u043a\u0443 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438. \u042f\u043a\u0449\u043e \u0431\u0443\u0434\u044c-\u044f\u043a\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u0431\u0443\u043b\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u0456 \u0443 \u0412\u0430\u0448\u0456\u0439 \u043c\u0435\u0440\u0435\u0436\u0456, \u0432\u043e\u043d\u0438 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u0456 \u043d\u0438\u0436\u0447\u0435. \n\n \u042f\u043a\u0449\u043e \u0412\u0438 \u043d\u0435 \u0431\u0430\u0447\u0438\u0442\u0435 \u0441\u0432\u0456\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0430\u0431\u043e \u0432\u0438\u043d\u0438\u043a\u0430\u044e\u0442\u044c \u0431\u0443\u0434\u044c-\u044f\u043a\u0456 \u0456\u043d\u0448\u0456 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u043f\u0456\u0434 \u0447\u0430\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0432\u043a\u0430\u0437\u0430\u0442\u0438 IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e. \n\n {devices}", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043d\u043e\u0432\u043e\u0433\u043e Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "\u041d\u0435 \u0432\u043c\u0438\u043a\u0430\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0443 Home Assistant" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/zh-Hans.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/zh-Hans.json new file mode 100644 index 00000000000..54095a0a633 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/zh-Hans.json @@ -0,0 +1,32 @@ +{ + "config": { + "step": { + "confirm": { + "description": "\u60a8\u5373\u5c06\u6dfb\u52a0 Apple TV (\u540d\u79f0\u4e3a\u201c{name}\u201d)\u5230 Home Assistant\u3002 \n\n **\u8981\u5b8c\u6210\u6b64\u8fc7\u7a0b\uff0c\u53ef\u80fd\u9700\u8981\u8f93\u5165\u591a\u4e2a PIN \u7801\u3002** \n\n\u8bf7\u6ce8\u610f\uff0c\u6b64\u96c6\u6210*\u4e0d\u80fd*\u5173\u95ed Apple TV \u7684\u7535\u6e90\uff0c\u53ea\u4f1a\u5173\u95ed Home Assistant \u4e2d\u7684\u5a92\u4f53\u64ad\u653e\u5668\uff01" + }, + "pair_no_pin": { + "title": "\u914d\u5bf9\u4e2d" + }, + "pair_with_pin": { + "data": { + "pin": "PIN\u7801" + } + }, + "user": { + "description": "\u8981\u5f00\u59cb\uff0c\u8bf7\u8f93\u5165\u8981\u6dfb\u52a0\u7684 Apple TV \u7684\u8bbe\u5907\u540d\u79f0\u6216 IP \u5730\u5740\u3002\u5728\u7f51\u7edc\u4e0a\u81ea\u52a8\u53d1\u73b0\u7684\u8bbe\u5907\u4f1a\u663e\u793a\u5728\u4e0b\u65b9\u3002 \n\n\u5982\u679c\u6ca1\u6709\u53d1\u73b0\u8bbe\u5907\u6216\u9047\u5230\u4efb\u4f55\u95ee\u9898\uff0c\u8bf7\u5c1d\u8bd5\u6307\u5b9a\u8bbe\u5907 IP \u5730\u5740\u3002 \n\n {devices}", + "title": "\u8bbe\u7f6e\u65b0\u7684 Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "\u542f\u52a8 Home Assistant \u65f6\u4e0d\u6253\u5f00\u8bbe\u5907" + }, + "description": "\u914d\u7f6e\u8bbe\u5907\u901a\u7528\u8bbe\u7f6e" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/zh-Hant.json new file mode 100644 index 00000000000..ced7a18d2a2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apple_tv/translations/zh-Hant.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "backoff": "\u88dd\u7f6e\u4e0d\u63a5\u53d7\u6b64\u6b21\u914d\u5c0d\u8acb\u6c42\uff08\u53ef\u80fd\u8f38\u5165\u592a\u591a\u6b21\u7121\u6548\u7684 PIN \u78bc\uff09\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66\u3002", + "device_did_not_pair": "\u88dd\u7f6e\u6c92\u6709\u5617\u8a66\u914d\u5c0d\u5b8c\u6210\u904e\u7a0b\u3002", + "invalid_config": "\u6b64\u88dd\u7f6e\u8a2d\u5b9a\u4e0d\u5b8c\u6574\uff0c\u8acb\u7a0d\u5019\u518d\u8a66\u4e00\u6b21\u3002", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "no_usable_service": "\u627e\u5230\u7684\u88dd\u7f6e\u7121\u6cd5\u8b58\u5225\u4ee5\u9032\u884c\u9023\u7dda\u3002\u5047\u5982\u6b64\u8a0a\u606f\u91cd\u8907\u767c\u751f\u3002\u8acb\u8a66\u8457\u6307\u5b9a\u7279\u5b9a IP \u4f4d\u5740\u6216\u91cd\u555f Apple TV\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u6b63\u8981\u65b0\u589e\u540d\u70ba `{name}` \u7684 Apple TV \u81f3 Home Assistant\u3002\n\n**\u6b32\u5b8c\u6210\u6b65\u9a5f\uff0c\u5fc5\u9808\u8f38\u5165\u591a\u7d44 PIN \u78bc\u3002**\n\n\u8acb\u6ce8\u610f\uff1a\u6b64\u6574\u5408\u4e26 *\u7121\u6cd5* \u9032\u884c Apple TV \u95dc\u6a5f\u7684\u52d5\u4f5c\uff0c\u50c5\u80fd\u65bc Home Assistant \u4e2d\u95dc\u9589\u5a92\u9ad4\u64ad\u653e\u5668\u529f\u80fd\uff01", + "title": "\u78ba\u8a8d\u65b0\u589e Apple TV" + }, + "pair_no_pin": { + "description": "`{protocol}` \u670d\u52d9\u9700\u8981\u9032\u884c\u914d\u5c0d\uff0c\u8acb\u8f38\u5165 Apple TV \u4e0a\u6240\u986f\u793a\u4e4b PIN {pin} \u4ee5\u7e7c\u7e8c\u3002", + "title": "\u914d\u5c0d\u4e2d" + }, + "pair_with_pin": { + "data": { + "pin": "PIN \u78bc" + }, + "description": "\u914d\u5c0d\u9700\u8981 `{protocol}` \u901a\u8a0a\u5354\u5b9a\u3002\u8acb\u8f38\u5165\u986f\u793a\u65bc\u756b\u9762\u4e0a\u7684 PIN \u78bc\uff0c\u524d\u65b9\u7684 0 \u53ef\u5ffd\u8996\u986f\u793a\u78bc\u70ba 0123\uff0c\u5247\u8f38\u5165 123\u3002", + "title": "\u914d\u5c0d\u4e2d" + }, + "reconfigure": { + "description": "\u6b64 Apple TV \u906d\u9047\u5230\u4e00\u4e9b\u9023\u7dda\u554f\u984c\uff0c\u5fc5\u9808\u91cd\u65b0\u8a2d\u5b9a\u3002", + "title": "\u88dd\u7f6e\u91cd\u65b0\u8a2d\u5b9a" + }, + "service_problem": { + "description": "\u7576\u914d\u5c0d `{protocol}` \u6642\u767c\u751f\u554f\u984c\uff0c\u5c07\u6703\u9032\u884c\u5ffd\u7565\u3002", + "title": "\u65b0\u589e\u670d\u52d9\u5931\u6557" + }, + "user": { + "data": { + "device_input": "\u88dd\u7f6e" + }, + "description": "\u9996\u5148\u8f38\u5165\u6240\u8981\u65b0\u589e\u7684 Apple TV \u88dd\u7f6e\u540d\u7a31\uff08\u4f8b\u5982\u5eda\u623f\u6216\u81e5\u5ba4\uff09\u6216 IP \u4f4d\u5740\u3002\u5047\u5982\u65bc\u5340\u7db2\u4e0a\u627e\u5230\u4efb\u4f55\u88dd\u7f6e\uff0c\u5c07\u6703\u986f\u793a\u65bc\u4e0b\u65b9\u3002\n\n\u5047\u5982\u7121\u6cd5\u770b\u5230\u88dd\u7f6e\u6216\u906d\u9047\u4efb\u4f55\u554f\u984c\uff0c\u8acb\u8a66\u8457\u6307\u5b9a\u88dd\u7f6e\u7684 IP \u4f4d\u5740\u3002\n\n{devices}", + "title": "\u8a2d\u5b9a\u4e00\u7d44 Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "\u7576\u958b\u59cb Home Assistant \u6642\u4e0d\u8981\u958b\u555f\u88dd\u7f6e" + }, + "description": "\u8a2d\u5b9a\u4e00\u822c\u88dd\u7f6e\u8a2d\u5b9a" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apprise/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/apprise/__init__.py new file mode 100644 index 00000000000..6ffdaf690d9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apprise/__init__.py @@ -0,0 +1 @@ +"""The apprise component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apprise/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/apprise/manifest.json new file mode 100644 index 00000000000..3e87d38ff69 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apprise/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "apprise", + "name": "Apprise", + "documentation": "https://www.home-assistant.io/integrations/apprise", + "requirements": ["apprise==0.9.3"], + "codeowners": ["@caronc"], + "iot_class": "cloud_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/apprise/notify.py b/homeassistant-2021.6.0.dev0/homeassistant/components/apprise/notify.py new file mode 100644 index 00000000000..2aeeb62b00b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/apprise/notify.py @@ -0,0 +1,69 @@ +"""Apprise platform for notify component.""" +import logging + +import apprise +import voluptuous as vol + +from homeassistant.components.notify import ( + ATTR_TARGET, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) +from homeassistant.const import CONF_URL +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_FILE = "config" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_URL): vol.All(cv.ensure_list, [str]), + vol.Optional(CONF_FILE): cv.string, + } +) + + +def get_service(hass, config, discovery_info=None): + """Get the Apprise notification service.""" + # Create our Apprise Instance (reference our asset) + a_obj = apprise.Apprise() + + if config.get(CONF_FILE): + # Sourced from a Configuration File + a_config = apprise.AppriseConfig() + if not a_config.add(config[CONF_FILE]): + _LOGGER.error("Invalid Apprise config url provided") + return None + + if not a_obj.add(a_config): + _LOGGER.error("Invalid Apprise config url provided") + return None + + # Ordered list of URLs + if config.get(CONF_URL) and not a_obj.add(config[CONF_URL]): + _LOGGER.error("Invalid Apprise URL(s) supplied") + return None + + return AppriseNotificationService(a_obj) + + +class AppriseNotificationService(BaseNotificationService): + """Implement the notification service for Apprise.""" + + def __init__(self, a_obj): + """Initialize the service.""" + self.apprise = a_obj + + def send_message(self, message="", **kwargs): + """Send a message to a specified target. + + If no target/tags are specified, then services are notified as is + However, if any tags are specified, then they will be applied + to the notification causing filtering (if set up that way). + """ + targets = kwargs.get(ATTR_TARGET) + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + self.apprise.notify(body=message, title=title, tag=targets) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aprs/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aprs/__init__.py new file mode 100644 index 00000000000..20a023166ae --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aprs/__init__.py @@ -0,0 +1 @@ +"""The APRS component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aprs/device_tracker.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aprs/device_tracker.py new file mode 100644 index 00000000000..1ce34c8a751 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aprs/device_tracker.py @@ -0,0 +1,183 @@ +"""Support for APRS device tracking.""" + +import logging +import threading + +import aprslib +from aprslib import ConnectionError as AprsConnectionError, LoginError +import geopy.distance +import voluptuous as vol + +from homeassistant.components.device_tracker import PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_GPS_ACCURACY, + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_HOST, + CONF_PASSWORD, + CONF_TIMEOUT, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.util import slugify + +DOMAIN = "aprs" + +_LOGGER = logging.getLogger(__name__) + +ATTR_ALTITUDE = "altitude" +ATTR_COURSE = "course" +ATTR_COMMENT = "comment" +ATTR_FROM = "from" +ATTR_FORMAT = "format" +ATTR_POS_AMBIGUITY = "posambiguity" +ATTR_SPEED = "speed" + +CONF_CALLSIGNS = "callsigns" + +DEFAULT_HOST = "rotate.aprs2.net" +DEFAULT_PASSWORD = "-1" +DEFAULT_TIMEOUT = 30.0 + +FILTER_PORT = 14580 + +MSG_FORMATS = ["compressed", "uncompressed", "mic-e"] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_CALLSIGNS): cv.ensure_list, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(float), + } +) + + +def make_filter(callsigns: list) -> str: + """Make a server-side filter from a list of callsigns.""" + return " ".join(f"b/{sign.upper()}" for sign in callsigns) + + +def gps_accuracy(gps, posambiguity: int) -> int: + """Calculate the GPS accuracy based on APRS posambiguity.""" + + pos_a_map = {0: 0, 1: 1 / 600, 2: 1 / 60, 3: 1 / 6, 4: 1} + if posambiguity in pos_a_map: + degrees = pos_a_map[posambiguity] + + gps2 = (gps[0], gps[1] + degrees) + dist_m = geopy.distance.distance(gps, gps2).m + + accuracy = round(dist_m) + else: + message = f"APRS position ambiguity must be 0-4, not '{posambiguity}'." + raise ValueError(message) + + return accuracy + + +def setup_scanner(hass, config, see, discovery_info=None): + """Set up the APRS tracker.""" + callsigns = config.get(CONF_CALLSIGNS) + server_filter = make_filter(callsigns) + + callsign = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + host = config.get(CONF_HOST) + timeout = config.get(CONF_TIMEOUT) + aprs_listener = AprsListenerThread(callsign, password, host, server_filter, see) + + def aprs_disconnect(event): + """Stop the APRS connection.""" + aprs_listener.stop() + + aprs_listener.start() + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, aprs_disconnect) + + if not aprs_listener.start_event.wait(timeout): + _LOGGER.error("Timeout waiting for APRS to connect") + return + + if not aprs_listener.start_success: + _LOGGER.error(aprs_listener.start_message) + return + + _LOGGER.debug(aprs_listener.start_message) + return True + + +class AprsListenerThread(threading.Thread): + """APRS message listener.""" + + def __init__( + self, callsign: str, password: str, host: str, server_filter: str, see + ): + """Initialize the class.""" + super().__init__() + + self.callsign = callsign + self.host = host + self.start_event = threading.Event() + self.see = see + self.server_filter = server_filter + self.start_message = "" + self.start_success = False + + self.ais = aprslib.IS( + self.callsign, passwd=password, host=self.host, port=FILTER_PORT + ) + + def start_complete(self, success: bool, message: str): + """Complete startup process.""" + self.start_message = message + self.start_success = success + self.start_event.set() + + def run(self): + """Connect to APRS and listen for data.""" + self.ais.set_filter(self.server_filter) + + try: + _LOGGER.info( + "Opening connection to %s with callsign %s", self.host, self.callsign + ) + self.ais.connect() + self.start_complete( + True, f"Connected to {self.host} with callsign {self.callsign}." + ) + self.ais.consumer(callback=self.rx_msg, immortal=True) + except (AprsConnectionError, LoginError) as err: + self.start_complete(False, str(err)) + except OSError: + _LOGGER.info( + "Closing connection to %s with callsign %s", self.host, self.callsign + ) + + def stop(self): + """Close the connection to the APRS network.""" + self.ais.close() + + def rx_msg(self, msg: dict): + """Receive message and process if position.""" + _LOGGER.debug("APRS message received: %s", str(msg)) + if msg[ATTR_FORMAT] in MSG_FORMATS: + dev_id = slugify(msg[ATTR_FROM]) + lat = msg[ATTR_LATITUDE] + lon = msg[ATTR_LONGITUDE] + + attrs = {} + if ATTR_POS_AMBIGUITY in msg: + pos_amb = msg[ATTR_POS_AMBIGUITY] + try: + attrs[ATTR_GPS_ACCURACY] = gps_accuracy((lat, lon), pos_amb) + except ValueError: + _LOGGER.warning( + "APRS message contained invalid posambiguity: %s", str(pos_amb) + ) + for attr in [ATTR_ALTITUDE, ATTR_COMMENT, ATTR_COURSE, ATTR_SPEED]: + if attr in msg: + attrs[attr] = msg[attr] + + self.see(dev_id=dev_id, gps=(lat, lon), attributes=attrs) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aprs/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aprs/manifest.json new file mode 100644 index 00000000000..29216e622da --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aprs/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "aprs", + "name": "APRS", + "documentation": "https://www.home-assistant.io/integrations/aprs", + "codeowners": ["@PhilRW"], + "requirements": ["aprslib==0.6.46", "geopy==2.1.0"], + "iot_class": "cloud_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aqualogic/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aqualogic/__init__.py new file mode 100644 index 00000000000..0878419a792 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aqualogic/__init__.py @@ -0,0 +1,90 @@ +"""Support for AquaLogic devices.""" +from datetime import timedelta +import logging +import threading +import time + +from aqualogic.core import AquaLogic +import voluptuous as vol + +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.helpers import config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "aqualogic" +UPDATE_TOPIC = f"{DOMAIN}_update" +CONF_UNIT = "unit" +RECONNECT_INTERVAL = timedelta(seconds=10) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + {vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.port} + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Set up AquaLogic platform.""" + host = config[DOMAIN][CONF_HOST] + port = config[DOMAIN][CONF_PORT] + processor = AquaLogicProcessor(hass, host, port) + hass.data[DOMAIN] = processor + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, processor.start_listen) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, processor.shutdown) + _LOGGER.debug("AquaLogicProcessor %s:%i initialized", host, port) + return True + + +class AquaLogicProcessor(threading.Thread): + """AquaLogic event processor thread.""" + + def __init__(self, hass, host, port): + """Initialize the data object.""" + super().__init__(daemon=True) + self._hass = hass + self._host = host + self._port = port + self._shutdown = False + self._panel = None + + def start_listen(self, event): + """Start event-processing thread.""" + _LOGGER.debug("Event processing thread started") + self.start() + + def shutdown(self, event): + """Signal shutdown of processing event.""" + _LOGGER.debug("Event processing signaled exit") + self._shutdown = True + + def data_changed(self, panel): + """Aqualogic data changed callback.""" + self._hass.helpers.dispatcher.dispatcher_send(UPDATE_TOPIC) + + def run(self): + """Event thread.""" + + while True: + self._panel = AquaLogic() + self._panel.connect(self._host, self._port) + self._panel.process(self.data_changed) + + if self._shutdown: + return + + _LOGGER.error("Connection to %s:%d lost", self._host, self._port) + time.sleep(RECONNECT_INTERVAL.total_seconds()) + + @property + def panel(self): + """Retrieve the AquaLogic object.""" + return self._panel diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aqualogic/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aqualogic/manifest.json new file mode 100644 index 00000000000..acae105b54d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aqualogic/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "aqualogic", + "name": "AquaLogic", + "documentation": "https://www.home-assistant.io/integrations/aqualogic", + "requirements": ["aqualogic==2.6"], + "codeowners": [], + "iot_class": "local_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aqualogic/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aqualogic/sensor.py new file mode 100644 index 00000000000..315b039f778 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aqualogic/sensor.py @@ -0,0 +1,111 @@ +"""Support for AquaLogic sensors.""" + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.const import ( + CONF_MONITORED_CONDITIONS, + PERCENTAGE, + POWER_WATT, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv + +from . import DOMAIN, UPDATE_TOPIC + +TEMP_UNITS = [TEMP_CELSIUS, TEMP_FAHRENHEIT] +PERCENT_UNITS = [PERCENTAGE, PERCENTAGE] +SALT_UNITS = ["g/L", "PPM"] +WATT_UNITS = [POWER_WATT, POWER_WATT] +NO_UNITS = [None, None] + +# sensor_type [ description, unit, icon ] +# sensor_type corresponds to property names in aqualogic.core.AquaLogic +SENSOR_TYPES = { + "air_temp": ["Air Temperature", TEMP_UNITS, "mdi:thermometer"], + "pool_temp": ["Pool Temperature", TEMP_UNITS, "mdi:oil-temperature"], + "spa_temp": ["Spa Temperature", TEMP_UNITS, "mdi:oil-temperature"], + "pool_chlorinator": ["Pool Chlorinator", PERCENT_UNITS, "mdi:gauge"], + "spa_chlorinator": ["Spa Chlorinator", PERCENT_UNITS, "mdi:gauge"], + "salt_level": ["Salt Level", SALT_UNITS, "mdi:gauge"], + "pump_speed": ["Pump Speed", PERCENT_UNITS, "mdi:speedometer"], + "pump_power": ["Pump Power", WATT_UNITS, "mdi:gauge"], + "status": ["Status", NO_UNITS, "mdi:alert"], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ) + } +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the sensor platform.""" + sensors = [] + + processor = hass.data[DOMAIN] + for sensor_type in config[CONF_MONITORED_CONDITIONS]: + sensors.append(AquaLogicSensor(processor, sensor_type)) + + async_add_entities(sensors) + + +class AquaLogicSensor(SensorEntity): + """Sensor implementation for the AquaLogic component.""" + + def __init__(self, processor, sensor_type): + """Initialize sensor.""" + self._processor = processor + self._type = sensor_type + self._state = None + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def name(self): + """Return the name of the sensor.""" + return f"AquaLogic {SENSOR_TYPES[self._type][0]}" + + @property + def unit_of_measurement(self): + """Return the unit of measurement the value is expressed in.""" + panel = self._processor.panel + if panel is None: + return None + if panel.is_metric: + return SENSOR_TYPES[self._type][1][0] + return SENSOR_TYPES[self._type][1][1] + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return SENSOR_TYPES[self._type][2] + + async def async_added_to_hass(self): + """Register callbacks.""" + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + UPDATE_TOPIC, self.async_update_callback + ) + ) + + @callback + def async_update_callback(self): + """Update callback.""" + panel = self._processor.panel + if panel is not None: + self._state = getattr(panel, self._type) + self.async_write_ha_state() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aqualogic/switch.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aqualogic/switch.py new file mode 100644 index 00000000000..08bba4cbd2d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aqualogic/switch.py @@ -0,0 +1,103 @@ +"""Support for AquaLogic switches.""" +from aqualogic.core import States +import voluptuous as vol + +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.const import CONF_MONITORED_CONDITIONS +import homeassistant.helpers.config_validation as cv + +from . import DOMAIN, UPDATE_TOPIC + +SWITCH_TYPES = { + "lights": "Lights", + "filter": "Filter", + "filter_low_speed": "Filter Low Speed", + "aux_1": "Aux 1", + "aux_2": "Aux 2", + "aux_3": "Aux 3", + "aux_4": "Aux 4", + "aux_5": "Aux 5", + "aux_6": "Aux 6", + "aux_7": "Aux 7", +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SWITCH_TYPES)): vol.All( + cv.ensure_list, [vol.In(SWITCH_TYPES)] + ) + } +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the switch platform.""" + switches = [] + + processor = hass.data[DOMAIN] + for switch_type in config[CONF_MONITORED_CONDITIONS]: + switches.append(AquaLogicSwitch(processor, switch_type)) + + async_add_entities(switches) + + +class AquaLogicSwitch(SwitchEntity): + """Switch implementation for the AquaLogic component.""" + + def __init__(self, processor, switch_type): + """Initialize switch.""" + self._processor = processor + self._type = switch_type + self._state_name = { + "lights": States.LIGHTS, + "filter": States.FILTER, + "filter_low_speed": States.FILTER_LOW_SPEED, + "aux_1": States.AUX_1, + "aux_2": States.AUX_2, + "aux_3": States.AUX_3, + "aux_4": States.AUX_4, + "aux_5": States.AUX_5, + "aux_6": States.AUX_6, + "aux_7": States.AUX_7, + }[switch_type] + + @property + def name(self): + """Return the name of the switch.""" + return f"AquaLogic {SWITCH_TYPES[self._type]}" + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def is_on(self): + """Return true if device is on.""" + panel = self._processor.panel + if panel is None: + return False + state = panel.get_state(self._state_name) + return state + + def turn_on(self, **kwargs): + """Turn the device on.""" + panel = self._processor.panel + if panel is None: + return + panel.set_state(self._state_name, True) + + def turn_off(self, **kwargs): + """Turn the device off.""" + panel = self._processor.panel + if panel is None: + return + panel.set_state(self._state_name, False) + + async def async_added_to_hass(self): + """Register callbacks.""" + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + UPDATE_TOPIC, self.async_write_ha_state + ) + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aquostv/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aquostv/__init__.py new file mode 100644 index 00000000000..a7f39037fe1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aquostv/__init__.py @@ -0,0 +1 @@ +"""The aquostv component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aquostv/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aquostv/manifest.json new file mode 100644 index 00000000000..a28c852d8db --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aquostv/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "aquostv", + "name": "Sharp Aquos TV", + "documentation": "https://www.home-assistant.io/integrations/aquostv", + "requirements": ["sharp_aquos_rc==0.3.2"], + "codeowners": [], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aquostv/media_player.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aquostv/media_player.py new file mode 100644 index 00000000000..35c7e2ae646 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aquostv/media_player.py @@ -0,0 +1,263 @@ +"""Support for interface with an Aquos TV.""" +import logging + +import sharp_aquos_rc +import voluptuous as vol + +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity +from homeassistant.components.media_player.const import ( + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, +) +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_TIMEOUT, + CONF_USERNAME, + STATE_OFF, + STATE_ON, +) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "Sharp Aquos TV" +DEFAULT_PORT = 10002 +DEFAULT_USERNAME = "admin" +DEFAULT_PASSWORD = "password" +DEFAULT_TIMEOUT = 0.5 +DEFAULT_RETRIES = 2 + +SUPPORT_SHARPTV = ( + SUPPORT_TURN_OFF + | SUPPORT_NEXT_TRACK + | SUPPORT_PAUSE + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_SELECT_SOURCE + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_STEP + | SUPPORT_VOLUME_SET + | SUPPORT_PLAY +) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.string, + vol.Optional("retries", default=DEFAULT_RETRIES): cv.string, + vol.Optional("power_on_enabled", default=False): cv.boolean, + } +) + +SOURCES = { + 0: "TV / Antenna", + 1: "HDMI_IN_1", + 2: "HDMI_IN_2", + 3: "HDMI_IN_3", + 4: "HDMI_IN_4", + 5: "COMPONENT IN", + 6: "VIDEO_IN_1", + 7: "VIDEO_IN_2", + 8: "PC_IN", +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Sharp Aquos TV platform.""" + + name = config[CONF_NAME] + port = config[CONF_PORT] + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + power_on_enabled = config["power_on_enabled"] + + if discovery_info: + _LOGGER.debug("%s", discovery_info) + vals = discovery_info.split(":") + if len(vals) > 1: + port = vals[1] + + host = vals[0] + remote = sharp_aquos_rc.TV(host, port, username, password, timeout=20) + add_entities([SharpAquosTVDevice(name, remote, power_on_enabled)]) + return True + + host = config[CONF_HOST] + remote = sharp_aquos_rc.TV(host, port, username, password, 15, 1) + + add_entities([SharpAquosTVDevice(name, remote, power_on_enabled)]) + return True + + +def _retry(func): + """Handle query retries.""" + + def wrapper(obj, *args, **kwargs): + """Wrap all query functions.""" + update_retries = 5 + while update_retries > 0: + try: + func(obj, *args, **kwargs) + break + except (OSError, TypeError, ValueError): + update_retries -= 1 + if update_retries == 0: + obj.set_state(STATE_OFF) + + return wrapper + + +class SharpAquosTVDevice(MediaPlayerEntity): + """Representation of a Aquos TV.""" + + def __init__(self, name, remote, power_on_enabled=False): + """Initialize the aquos device.""" + self._supported_features = SUPPORT_SHARPTV + self._power_on_enabled = power_on_enabled + if self._power_on_enabled: + self._supported_features |= SUPPORT_TURN_ON + # Save a reference to the imported class + self._name = name + # Assume that the TV is not muted + self._muted = False + self._state = None + self._remote = remote + self._volume = 0 + self._source = None + self._source_list = list(SOURCES.values()) + + def set_state(self, state): + """Set TV state.""" + self._state = state + + @_retry + def update(self): + """Retrieve the latest data.""" + if self._remote.power() == 1: + self._state = STATE_ON + else: + self._state = STATE_OFF + # Set TV to be able to remotely power on + if self._power_on_enabled: + self._remote.power_on_command_settings(2) + else: + self._remote.power_on_command_settings(0) + # Get mute state + if self._remote.mute() == 2: + self._muted = False + else: + self._muted = True + # Get source + self._source = SOURCES.get(self._remote.input()) + # Get volume + self._volume = self._remote.volume() / 60 + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def source(self): + """Return the current source.""" + return self._source + + @property + def source_list(self): + """Return the source list.""" + return self._source_list + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + return self._volume + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._muted + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return self._supported_features + + @_retry + def turn_off(self): + """Turn off tvplayer.""" + self._remote.power(0) + + @_retry + def volume_up(self): + """Volume up the media player.""" + self._remote.volume(int(self._volume * 60) + 2) + + @_retry + def volume_down(self): + """Volume down media player.""" + self._remote.volume(int(self._volume * 60) - 2) + + @_retry + def set_volume_level(self, volume): + """Set Volume media player.""" + self._remote.volume(int(volume * 60)) + + @_retry + def mute_volume(self, mute): + """Send mute command.""" + self._remote.mute(0) + + @_retry + def turn_on(self): + """Turn the media player on.""" + self._remote.power(1) + + @_retry + def media_play_pause(self): + """Simulate play pause media player.""" + self._remote.remote_button(40) + + @_retry + def media_play(self): + """Send play command.""" + self._remote.remote_button(16) + + @_retry + def media_pause(self): + """Send pause command.""" + self._remote.remote_button(16) + + @_retry + def media_next_track(self): + """Send next track command.""" + self._remote.remote_button(21) + + @_retry + def media_previous_track(self): + """Send the previous track command.""" + self._remote.remote_button(19) + + def select_source(self, source): + """Set the input source.""" + for key, value in SOURCES.items(): + if source == value: + self._remote.input(key) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/__init__.py new file mode 100644 index 00000000000..e1dfac09d76 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/__init__.py @@ -0,0 +1,113 @@ +"""Arcam component.""" +import asyncio +from contextlib import suppress +import logging + +from arcam.fmj import ConnectionFailed +from arcam.fmj.client import Client +import async_timeout + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType + +from .const import ( + DEFAULT_SCAN_INTERVAL, + DOMAIN, + DOMAIN_DATA_ENTRIES, + DOMAIN_DATA_TASKS, + SIGNAL_CLIENT_DATA, + SIGNAL_CLIENT_STARTED, + SIGNAL_CLIENT_STOPPED, +) + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = cv.deprecated(DOMAIN) + +PLATFORMS = ["media_player"] + + +async def _await_cancel(task): + task.cancel() + with suppress(asyncio.CancelledError): + await task + + +async def async_setup(hass: HomeAssistant, config: ConfigType): + """Set up the component.""" + hass.data[DOMAIN_DATA_ENTRIES] = {} + hass.data[DOMAIN_DATA_TASKS] = {} + + async def _stop(_): + asyncio.gather( + *[_await_cancel(task) for task in hass.data[DOMAIN_DATA_TASKS].values()] + ) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): + """Set up config entry.""" + entries = hass.data[DOMAIN_DATA_ENTRIES] + tasks = hass.data[DOMAIN_DATA_TASKS] + + client = Client(entry.data[CONF_HOST], entry.data[CONF_PORT]) + entries[entry.entry_id] = client + + task = asyncio.create_task(_run_client(hass, client, DEFAULT_SCAN_INTERVAL)) + tasks[entry.entry_id] = task + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass, entry): + """Cleanup before removing config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + task = hass.data[DOMAIN_DATA_TASKS].pop(entry.entry_id) + await _await_cancel(task) + + hass.data[DOMAIN_DATA_ENTRIES].pop(entry.entry_id) + + return unload_ok + + +async def _run_client(hass, client, interval): + def _listen(_): + hass.helpers.dispatcher.async_dispatcher_send(SIGNAL_CLIENT_DATA, client.host) + + while True: + try: + with async_timeout.timeout(interval): + await client.start() + + _LOGGER.debug("Client connected %s", client.host) + hass.helpers.dispatcher.async_dispatcher_send( + SIGNAL_CLIENT_STARTED, client.host + ) + + try: + with client.listen(_listen): + await client.process() + finally: + await client.stop() + + _LOGGER.debug("Client disconnected %s", client.host) + hass.helpers.dispatcher.async_dispatcher_send( + SIGNAL_CLIENT_STOPPED, client.host + ) + + except ConnectionFailed: + await asyncio.sleep(interval) + except asyncio.TimeoutError: + continue + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception, aborting arcam client") + return diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/config_flow.py new file mode 100644 index 00000000000..cbf707c14e6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/config_flow.py @@ -0,0 +1,98 @@ +"""Config flow to configure the Arcam FMJ component.""" +from urllib.parse import urlparse + +from arcam.fmj.client import Client, ConnectionFailed +from arcam.fmj.utils import get_uniqueid_from_host, get_uniqueid_from_udn +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_UDN +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN, DOMAIN_DATA_ENTRIES + + +def get_entry_client(hass, entry): + """Retrieve client associated with a config entry.""" + return hass.data[DOMAIN_DATA_ENTRIES][entry.entry_id] + + +class ArcamFmjFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle config flow.""" + + VERSION = 1 + + async def _async_set_unique_id_and_update(self, host, port, uuid): + await self.async_set_unique_id(uuid) + self._abort_if_unique_id_configured({CONF_HOST: host, CONF_PORT: port}) + + async def _async_check_and_create(self, host, port): + client = Client(host, port) + try: + await client.start() + except ConnectionFailed: + return self.async_abort(reason="cannot_connect") + finally: + await client.stop() + + return self.async_create_entry( + title=f"{DEFAULT_NAME} ({host})", + data={CONF_HOST: host, CONF_PORT: port}, + ) + + async def async_step_user(self, user_input=None): + """Handle a discovered device.""" + errors = {} + + if user_input is not None: + uuid = await get_uniqueid_from_host( + async_get_clientsession(self.hass), user_input[CONF_HOST] + ) + if uuid: + await self._async_set_unique_id_and_update( + user_input[CONF_HOST], user_input[CONF_PORT], uuid + ) + + return await self._async_check_and_create( + user_input[CONF_HOST], user_input[CONF_PORT] + ) + + fields = { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } + + return self.async_show_form( + step_id="user", data_schema=vol.Schema(fields), errors=errors + ) + + async def async_step_confirm(self, user_input=None): + """Handle user-confirmation of discovered node.""" + context = self.context + placeholders = { + "host": context[CONF_HOST], + } + context["title_placeholders"] = placeholders + + if user_input is not None: + return await self._async_check_and_create( + context[CONF_HOST], context[CONF_PORT] + ) + + return self.async_show_form( + step_id="confirm", description_placeholders=placeholders + ) + + async def async_step_ssdp(self, discovery_info): + """Handle a discovered device.""" + host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname + port = DEFAULT_PORT + uuid = get_uniqueid_from_udn(discovery_info[ATTR_UPNP_UDN]) + + await self._async_set_unique_id_and_update(host, port, uuid) + + context = self.context + context[CONF_HOST] = host + context[CONF_PORT] = DEFAULT_PORT + return await self.async_step_confirm() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/const.py new file mode 100644 index 00000000000..9f837c94bcd --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/const.py @@ -0,0 +1,15 @@ +"""Constants used for arcam.""" +DOMAIN = "arcam_fmj" + +SIGNAL_CLIENT_STARTED = "arcam.client_started" +SIGNAL_CLIENT_STOPPED = "arcam.client_stopped" +SIGNAL_CLIENT_DATA = "arcam.client_data" + +EVENT_TURN_ON = "arcam_fmj.turn_on" + +DEFAULT_PORT = 50000 +DEFAULT_NAME = "Arcam FMJ" +DEFAULT_SCAN_INTERVAL = 5 + +DOMAIN_DATA_ENTRIES = f"{DOMAIN}.entries" +DOMAIN_DATA_TASKS = f"{DOMAIN}.tasks" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/device_trigger.py b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/device_trigger.py new file mode 100644 index 00000000000..4ae34abb2c2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/device_trigger.py @@ -0,0 +1,82 @@ +"""Provides device automations for Arcam FMJ Receiver control.""" +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, +) +from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType + +from .const import DOMAIN, EVENT_TURN_ON + +TRIGGER_TYPES = {"turn_on"} +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } +) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: + """List device triggers for Arcam FMJ Receiver control devices.""" + registry = await entity_registry.async_get_registry(hass) + triggers = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain == "media_player": + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turn_on", + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + trigger_id = automation_info.get("trigger_id") if automation_info else None + job = HassJob(action) + + if config[CONF_TYPE] == "turn_on": + entity_id = config[CONF_ENTITY_ID] + + @callback + def _handle_event(event: Event): + if event.data[ATTR_ENTITY_ID] == entity_id: + hass.async_run_hass_job( + job, + { + "trigger": { + **config, + "description": f"{DOMAIN} - {entity_id}", + "id": trigger_id, + } + }, + event.context, + ) + + return hass.bus.async_listen(EVENT_TURN_ON, _handle_event) + + return lambda: None diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/manifest.json new file mode 100644 index 00000000000..d38ceceba73 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/manifest.json @@ -0,0 +1,15 @@ +{ + "domain": "arcam_fmj", + "name": "Arcam FMJ Receivers", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/arcam_fmj", + "requirements": ["arcam-fmj==0.5.3"], + "ssdp": [ + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", + "manufacturer": "ARCAM" + } + ], + "codeowners": ["@elupus"], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/media_player.py b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/media_player.py new file mode 100644 index 00000000000..8a119d020fe --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/media_player.py @@ -0,0 +1,405 @@ +"""Arcam media player.""" +import logging + +from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceCodes +from arcam.fmj.state import State + +from homeassistant import config_entries +from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_MUSIC, + MEDIA_TYPE_MUSIC, + SUPPORT_BROWSE_MEDIA, + SUPPORT_PLAY_MEDIA, + SUPPORT_SELECT_SOUND_MODE, + SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, +) +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant, callback + +from .config_flow import get_entry_client +from .const import ( + DOMAIN, + EVENT_TURN_ON, + SIGNAL_CLIENT_DATA, + SIGNAL_CLIENT_STARTED, + SIGNAL_CLIENT_STOPPED, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: config_entries.ConfigEntry, + async_add_entities, +): + """Set up the configuration entry.""" + + client = get_entry_client(hass, config_entry) + + async_add_entities( + [ + ArcamFmj( + config_entry.title, + State(client, zone), + config_entry.unique_id or config_entry.entry_id, + ) + for zone in [1, 2] + ], + True, + ) + + return True + + +class ArcamFmj(MediaPlayerEntity): + """Representation of a media device.""" + + def __init__( + self, + device_name, + state: State, + uuid: str, + ): + """Initialize device.""" + self._state = state + self._device_name = device_name + self._name = f"{device_name} - Zone: {state.zn}" + self._uuid = uuid + self._support = ( + SUPPORT_SELECT_SOURCE + | SUPPORT_PLAY_MEDIA + | SUPPORT_BROWSE_MEDIA + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_STEP + | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON + ) + if state.zn == 1: + self._support |= SUPPORT_SELECT_SOUND_MODE + + def _get_2ch(self): + """Return if source is 2 channel or not.""" + audio_format, _ = self._state.get_incoming_audio_format() + return bool( + audio_format + in ( + IncomingAudioFormat.PCM, + IncomingAudioFormat.ANALOGUE_DIRECT, + IncomingAudioFormat.UNDETECTED, + None, + ) + ) + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self._state.zn == 1 + + @property + def unique_id(self): + """Return unique identifier if known.""" + return f"{self._uuid}-{self._state.zn}" + + @property + def device_info(self): + """Return a device description for device registry.""" + return { + "name": self._device_name, + "identifiers": { + (DOMAIN, self._uuid), + (DOMAIN, self._state.client.host, self._state.client.port), + }, + "model": "Arcam FMJ AVR", + "manufacturer": "Arcam", + } + + @property + def should_poll(self) -> bool: + """No need to poll.""" + return False + + @property + def name(self): + """Return the name of the controlled device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + if self._state.get_power(): + return STATE_ON + return STATE_OFF + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return self._support + + async def async_added_to_hass(self): + """Once registered, add listener for events.""" + await self._state.start() + + @callback + def _data(host): + if host == self._state.client.host: + self.async_write_ha_state() + + @callback + def _started(host): + if host == self._state.client.host: + self.async_schedule_update_ha_state(force_refresh=True) + + @callback + def _stopped(host): + if host == self._state.client.host: + self.async_schedule_update_ha_state(force_refresh=True) + + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_CLIENT_DATA, _data + ) + ) + + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_CLIENT_STARTED, _started + ) + ) + + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_CLIENT_STOPPED, _stopped + ) + ) + + async def async_update(self): + """Force update of state.""" + _LOGGER.debug("Update state %s", self.name) + await self._state.update() + + async def async_mute_volume(self, mute): + """Send mute command.""" + await self._state.set_mute(mute) + self.async_write_ha_state() + + async def async_select_source(self, source): + """Select a specific source.""" + try: + value = SourceCodes[source] + except KeyError: + _LOGGER.error("Unsupported source %s", source) + return + + await self._state.set_source(value) + self.async_write_ha_state() + + async def async_select_sound_mode(self, sound_mode): + """Select a specific source.""" + try: + if self._get_2ch(): + await self._state.set_decode_mode_2ch(DecodeMode2CH[sound_mode]) + else: + await self._state.set_decode_mode_mch(DecodeModeMCH[sound_mode]) + except KeyError: + _LOGGER.error("Unsupported sound_mode %s", sound_mode) + return + + self.async_write_ha_state() + + async def async_set_volume_level(self, volume): + """Set volume level, range 0..1.""" + await self._state.set_volume(round(volume * 99.0)) + self.async_write_ha_state() + + async def async_volume_up(self): + """Turn volume up for media player.""" + await self._state.inc_volume() + self.async_write_ha_state() + + async def async_volume_down(self): + """Turn volume up for media player.""" + await self._state.dec_volume() + self.async_write_ha_state() + + async def async_turn_on(self): + """Turn the media player on.""" + if self._state.get_power() is not None: + _LOGGER.debug("Turning on device using connection") + await self._state.set_power(True) + else: + _LOGGER.debug("Firing event to turn on device") + self.hass.bus.async_fire(EVENT_TURN_ON, {ATTR_ENTITY_ID: self.entity_id}) + + async def async_turn_off(self): + """Turn the media player off.""" + await self._state.set_power(False) + + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + if media_content_id not in (None, "root"): + raise BrowseError( + f"Media not found: {media_content_type} / {media_content_id}" + ) + + presets = self._state.get_preset_details() + + radio = [ + BrowseMedia( + title=preset.name, + media_class=MEDIA_CLASS_MUSIC, + media_content_id=f"preset:{preset.index}", + media_content_type=MEDIA_TYPE_MUSIC, + can_play=True, + can_expand=False, + ) + for preset in presets.values() + ] + + root = BrowseMedia( + title="Root", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_id="root", + media_content_type="library", + can_play=False, + can_expand=True, + children=radio, + ) + + return root + + async def async_play_media(self, media_type: str, media_id: str, **kwargs) -> None: + """Play media.""" + + if media_id.startswith("preset:"): + preset = int(media_id[7:]) + await self._state.set_tuner_preset(preset) + else: + _LOGGER.error("Media %s is not supported", media_id) + return + + @property + def source(self): + """Return the current input source.""" + value = self._state.get_source() + if value is None: + return None + return value.name + + @property + def source_list(self): + """List of available input sources.""" + return [x.name for x in self._state.get_source_list()] + + @property + def sound_mode(self): + """Name of the current sound mode.""" + if self._state.zn != 1: + return None + + if self._get_2ch(): + value = self._state.get_decode_mode_2ch() + else: + value = self._state.get_decode_mode_mch() + if value: + return value.name + return None + + @property + def sound_mode_list(self): + """List of available sound modes.""" + if self._state.zn != 1: + return None + + if self._get_2ch(): + return [x.name for x in DecodeMode2CH] + return [x.name for x in DecodeModeMCH] + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + value = self._state.get_mute() + if value is None: + return None + return value + + @property + def volume_level(self): + """Volume level of device.""" + value = self._state.get_volume() + if value is None: + return None + return value / 99.0 + + @property + def media_content_type(self): + """Content type of current playing media.""" + source = self._state.get_source() + if source == SourceCodes.DAB: + value = MEDIA_TYPE_MUSIC + elif source == SourceCodes.FM: + value = MEDIA_TYPE_MUSIC + else: + value = None + return value + + @property + def media_content_id(self): + """Content type of current playing media.""" + source = self._state.get_source() + if source in (SourceCodes.DAB, SourceCodes.FM): + preset = self._state.get_tuner_preset() + if preset: + value = f"preset:{preset}" + else: + value = None + else: + value = None + + return value + + @property + def media_channel(self): + """Channel currently playing.""" + source = self._state.get_source() + if source == SourceCodes.DAB: + value = self._state.get_dab_station() + elif source == SourceCodes.FM: + value = self._state.get_rds_information() + else: + value = None + return value + + @property + def media_artist(self): + """Artist of current playing media, music track only.""" + source = self._state.get_source() + if source == SourceCodes.DAB: + value = self._state.get_dls_pdt() + else: + value = None + return value + + @property + def media_title(self): + """Title of current playing media.""" + source = self._state.get_source() + if source is None: + return None + + channel = self.media_channel + + if channel: + value = f"{source.name} - {channel}" + else: + value = source.name + return value diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/strings.json new file mode 100644 index 00000000000..154727baf9f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/strings.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "error": {}, + "flow_title": "{host}", + "step": { + "confirm": { + "description": "Do you want to add Arcam FMJ on `{host}` to Home Assistant?" + }, + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "description": "Please enter the host name or IP address of device." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} was requested to turn on" + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/ca.json new file mode 100644 index 00000000000..84ed4dd8990 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "flow_title": "{host}", + "step": { + "confirm": { + "description": "Vols afegir l'Arcam FMJ `{host}` a Home Assistant?" + }, + "user": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port" + }, + "description": "Introdueix el nom de l'amfitri\u00f3 o l'adre\u00e7a IP del dispositiu." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "S'ha sol\u00b7licitat l'activaci\u00f3 de {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/cs.json new file mode 100644 index 00000000000..c5909f14e05 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/cs.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "flow_title": "Arcam FMJ na {host}", + "step": { + "confirm": { + "description": "Chcete p\u0159idat Arcam FMJ na `{host}` do Home Assistant?" + }, + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + }, + "description": "Zadejte n\u00e1zev hostitele nebo IP adresu za\u0159\u00edzen\u00ed." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} bylo po\u017e\u00e1d\u00e1no o zapnut\u00ed" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/de.json new file mode 100644 index 00000000000..1f67a8d30a9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/de.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "flow_title": "Arcam FMJ auf {host}", + "step": { + "confirm": { + "description": "M\u00f6chtest du Arcam FMJ auf `{host}` zum Home Assistant hinzuf\u00fcgen?" + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Bitte gib den Hostnamen oder die IP-Adresse des Ger\u00e4ts ein." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} wurde zum Einschalten aufgefordert" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/en.json new file mode 100644 index 00000000000..891f268aa1f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "cannot_connect": "Failed to connect" + }, + "flow_title": "{host}", + "step": { + "confirm": { + "description": "Do you want to add Arcam FMJ on `{host}` to Home Assistant?" + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Please enter the host name or IP address of device." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} was requested to turn on" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/es.json new file mode 100644 index 00000000000..6959ee85ab1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/es.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "cannot_connect": "No se pudo conectar" + }, + "flow_title": "Arcam FMJ en {host}", + "step": { + "confirm": { + "description": "\u00bfQuieres a\u00f1adir el Arcam FMJ en `{host}` a Home Assistant?" + }, + "user": { + "data": { + "host": "Host", + "port": "Puerto" + }, + "description": "Por favor, introduce el nombre del host o la direcci\u00f3n IP del dispositivo." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "Se solicit\u00f3 encender {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/et.json new file mode 100644 index 00000000000..60f1895039e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/et.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "cannot_connect": "\u00dchendamine nurjus" + }, + "flow_title": "{host}", + "step": { + "confirm": { + "description": "Kas soovid lisada Arcam FMJ \u00fcksuse {host} Home Assistanti?" + }, + "user": { + "data": { + "host": "", + "port": "" + }, + "description": "Sisesta seadme hostinimi v\u00f5i IP-aadress." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} paluti sisse l\u00fclitada" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/fr.json new file mode 100644 index 00000000000..511d9e98a50 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/fr.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil \u00e9tait d\u00e9j\u00e0 configur\u00e9.", + "already_in_progress": "Le flux de configuration de l'appareil est d\u00e9j\u00e0 en cours.", + "cannot_connect": "\u00c9chec de connexion" + }, + "error": { + "one": "Vide", + "other": "Vide" + }, + "flow_title": "Arcam FMJ sur {host}", + "step": { + "confirm": { + "description": "Voulez-vous ajouter Arcam FMJ sur ` {host} ` \u00e0 HomeAssistant ?" + }, + "user": { + "data": { + "host": "H\u00f4te", + "port": "Port" + }, + "description": "Veuillez saisir le nom d\u2019h\u00f4te ou l\u2019adresse IP du p\u00e9riph\u00e9rique." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "Il a \u00e9t\u00e9 demand\u00e9 \u00e0 {nom_de_l'entit\u00e9} de s'allumer" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/hu.json new file mode 100644 index 00000000000..4af1181a265 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/hu.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "Hoszt", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/id.json new file mode 100644 index 00000000000..96b10140948 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/id.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "Arcam FMJ di {host}", + "step": { + "confirm": { + "description": "Ingin menambahkan Arcam FMJ `{host}` ke Home Assistant?" + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Masukkan nama host atau alamat IP perangkat." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} diminta untuk dinyalakan" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/it.json new file mode 100644 index 00000000000..24c9b99e7a8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "cannot_connect": "Impossibile connettersi" + }, + "flow_title": "{host}", + "step": { + "confirm": { + "description": "Vuoi aggiungere Arcam FMJ su `{host}` a Home Assistant?" + }, + "user": { + "data": { + "host": "Host", + "port": "Porta" + }, + "description": "Inserisci il nome host o l'indirizzo IP del dispositivo." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "\u00c8 stato richiesto di attivare {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/ko.json new file mode 100644 index 00000000000..29a2887f7e2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/ko.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "Arcam FMJ: {host}", + "step": { + "confirm": { + "description": "Home Assistant\uc5d0 Arcam FMJ `{host}`\uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + }, + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8" + }, + "description": "\uae30\uae30\uc758 \ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name}\uc774(\uac00) \ucf1c\uc9c0\ub3c4\ub85d \uc694\uccad\ub418\uc5c8\uc744 \ub54c" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/lb.json new file mode 100644 index 00000000000..45b9e6fd8a6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/lb.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "already_in_progress": "Konfiguratioun's Oflaf ass schonn am gaangen.", + "cannot_connect": "Feeler beim verbannen" + }, + "flow_title": "Arcam FMJ um {host}", + "step": { + "confirm": { + "description": "Soll den Arcam FMJ um `{host}` am Home Assistant dob\u00e4i gesaat ginn?" + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "G\u00ebff den Numm oder IP-Adress vum Apparat un." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} soll ugeschalt ginn" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/nl.json new file mode 100644 index 00000000000..45a7be867b9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/nl.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", + "cannot_connect": "Kan geen verbinding maken" + }, + "error": { + "one": "Leeg", + "other": "Leeg" + }, + "flow_title": "{host}", + "step": { + "confirm": { + "description": "Wil je Arcam FMJ op `{host}` toevoegen aan Home Assistant?" + }, + "user": { + "data": { + "host": "Host", + "port": "Poort" + }, + "description": "Voer de hostnaam of het IP-adres van het apparaat in." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} is gevraagd in te schakelen" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/no.json new file mode 100644 index 00000000000..8e4d28d80b8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/no.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "cannot_connect": "Tilkobling mislyktes" + }, + "flow_title": "{host}", + "step": { + "confirm": { + "description": "Vil du legge Arcam FMJ p\u00e5 `{host}` til Home Assistant?" + }, + "user": { + "data": { + "host": "Vert", + "port": "Port" + }, + "description": "Vennligst skriv inn vertsnavnet eller IP-adressen til enheten." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} ble bedt om \u00e5 sl\u00e5 p\u00e5" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/pl.json new file mode 100644 index 00000000000..1373aae5cf2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/pl.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "error": { + "few": "kilka", + "many": "wiele", + "one": "jeden", + "other": "inne" + }, + "flow_title": "{host}", + "step": { + "confirm": { + "description": "Czy chcesz doda\u0107 Arcam FMJ na \"{host}\" do Home Assistanta?" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port" + }, + "description": "Wpisz nazw\u0119 hosta lub adres IP urz\u0105dzenia." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} zostanie poproszony o w\u0142\u0105czenie" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/pt-BR.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/pt-BR.json new file mode 100644 index 00000000000..8071efb001f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/pt-BR.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "trigger_type": { + "turn_on": "Foi solicitado que {entity_name} ligue" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/pt.json new file mode 100644 index 00000000000..af72dfe96e2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/pt.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { + "one": "uma", + "other": "mais" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porto" + }, + "description": "Por favor, introduza o nome ou o endere\u00e7o IP do dispositivo." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/ro.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/ro.json new file mode 100644 index 00000000000..a8008f1e8bc --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/ro.json @@ -0,0 +1,9 @@ +{ + "config": { + "error": { + "few": "Pu\u021bine", + "one": "Unul", + "other": "Altele" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/ru.json new file mode 100644 index 00000000000..20f44f068de --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/ru.json @@ -0,0 +1,27 @@ +{ + "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_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "flow_title": "{host}", + "step": { + "confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c Arcam FMJ `{host}`?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/tr.json new file mode 100644 index 00000000000..dd15f57212c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/uk.json new file mode 100644 index 00000000000..4d33a5bc0d9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/uk.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "flow_title": "Arcam FMJ {host}", + "step": { + "confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 Arcam FMJ `{host}`?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "\u0437\u0430\u043f\u0438\u0442\u0430\u043d\u043e \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u044f {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/zh-Hans.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/zh-Hans.json new file mode 100644 index 00000000000..6e842e66fab --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/zh-Hant.json new file mode 100644 index 00000000000..358805e0de6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arcam_fmj/translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "flow_title": "{host}", + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u5c07 Arcam FMJ `{host}` \u65b0\u589e\u81f3 Home Assistant\uff1f" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u8acb\u8f38\u5165\u4e3b\u6a5f\u7aef\u540d\u7a31\u6216 Heos \u88dd\u7f6e IP \u4f4d\u5740\u3002" + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} \u4f9d\u9700\u6c42\u958b\u555f" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arduino/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/arduino/__init__.py new file mode 100644 index 00000000000..2890fd4abda --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arduino/__init__.py @@ -0,0 +1,115 @@ +"""Support for Arduino boards running with the Firmata firmware.""" +import logging + +from PyMata.pymata import PyMata +import serial +import voluptuous as vol + +from homeassistant.const import ( + CONF_PORT, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, +) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "arduino" + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema({vol.Required(CONF_PORT): cv.string})}, extra=vol.ALLOW_EXTRA +) + + +def setup(hass, config): + """Set up the Arduino component.""" + _LOGGER.warning( + "The %s integration has been deprecated. Please move your " + "configuration to the firmata integration. " + "https://www.home-assistant.io/integrations/firmata", + DOMAIN, + ) + + port = config[DOMAIN][CONF_PORT] + + try: + board = ArduinoBoard(port) + except (serial.serialutil.SerialException, FileNotFoundError): + _LOGGER.error("Your port %s is not accessible", port) + return False + + try: + if board.get_firmata()[1] <= 2: + _LOGGER.error("The StandardFirmata sketch should be 2.2 or newer") + return False + except IndexError: + _LOGGER.warning( + "The version of the StandardFirmata sketch was not" + "detected. This may lead to side effects" + ) + + def stop_arduino(event): + """Stop the Arduino service.""" + board.disconnect() + + def start_arduino(event): + """Start the Arduino service.""" + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_arduino) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_arduino) + hass.data[DOMAIN] = board + + return True + + +class ArduinoBoard: + """Representation of an Arduino board.""" + + def __init__(self, port): + """Initialize the board.""" + + self._port = port + self._board = PyMata(self._port, verbose=False) + + def set_mode(self, pin, direction, mode): + """Set the mode and the direction of a given pin.""" + if mode == "analog" and direction == "in": + self._board.set_pin_mode(pin, self._board.INPUT, self._board.ANALOG) + elif mode == "analog" and direction == "out": + self._board.set_pin_mode(pin, self._board.OUTPUT, self._board.ANALOG) + elif mode == "digital" and direction == "in": + self._board.set_pin_mode(pin, self._board.INPUT, self._board.DIGITAL) + elif mode == "digital" and direction == "out": + self._board.set_pin_mode(pin, self._board.OUTPUT, self._board.DIGITAL) + elif mode == "pwm": + self._board.set_pin_mode(pin, self._board.OUTPUT, self._board.PWM) + + def get_analog_inputs(self): + """Get the values from the pins.""" + self._board.capability_query() + return self._board.get_analog_response_table() + + def set_digital_out_high(self, pin): + """Set a given digital pin to high.""" + self._board.digital_write(pin, 1) + + def set_digital_out_low(self, pin): + """Set a given digital pin to low.""" + self._board.digital_write(pin, 0) + + def get_digital_in(self, pin): + """Get the value from a given digital pin.""" + self._board.digital_read(pin) + + def get_analog_in(self, pin): + """Get the value from a given analog pin.""" + self._board.analog_read(pin) + + def get_firmata(self): + """Return the version of the Firmata firmware.""" + return self._board.get_firmata_version() + + def disconnect(self): + """Disconnect the board and close the serial connection.""" + self._board.reset() + self._board.close() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arduino/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arduino/manifest.json new file mode 100644 index 00000000000..95764ebb913 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arduino/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "arduino", + "name": "Arduino", + "documentation": "https://www.home-assistant.io/integrations/arduino", + "requirements": ["PyMata==2.20"], + "codeowners": ["@fabaff"], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arduino/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/arduino/sensor.py new file mode 100644 index 00000000000..588a652660a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arduino/sensor.py @@ -0,0 +1,58 @@ +"""Support for getting information from Arduino pins.""" +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv + +from . import DOMAIN + +CONF_PINS = "pins" +CONF_TYPE = "analog" + +PIN_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_PINS): vol.Schema({cv.positive_int: PIN_SCHEMA})} +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Arduino platform.""" + board = hass.data[DOMAIN] + + pins = config[CONF_PINS] + + sensors = [] + for pinnum, pin in pins.items(): + sensors.append(ArduinoSensor(pin.get(CONF_NAME), pinnum, CONF_TYPE, board)) + add_entities(sensors) + + +class ArduinoSensor(SensorEntity): + """Representation of an Arduino Sensor.""" + + def __init__(self, name, pin, pin_type, board): + """Initialize the sensor.""" + self._pin = pin + self._name = name + self.pin_type = pin_type + self.direction = "in" + self._value = None + + board.set_mode(self._pin, self.direction, self.pin_type) + self._board = board + + @property + def state(self): + """Return the state of the sensor.""" + return self._value + + @property + def name(self): + """Get the name of the sensor.""" + return self._name + + def update(self): + """Get the latest value from the pin.""" + self._value = self._board.get_analog_inputs()[self._pin][1] diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arduino/switch.py b/homeassistant-2021.6.0.dev0/homeassistant/components/arduino/switch.py new file mode 100644 index 00000000000..6ee742fd506 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arduino/switch.py @@ -0,0 +1,80 @@ +"""Support for switching Arduino pins on and off.""" +import voluptuous as vol + +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv + +from . import DOMAIN + +CONF_PINS = "pins" +CONF_TYPE = "digital" +CONF_NEGATE = "negate" +CONF_INITIAL = "initial" + +PIN_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_INITIAL, default=False): cv.boolean, + vol.Optional(CONF_NEGATE, default=False): cv.boolean, + } +) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_PINS, default={}): vol.Schema({cv.positive_int: PIN_SCHEMA})} +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Arduino platform.""" + board = hass.data[DOMAIN] + + pins = config[CONF_PINS] + + switches = [] + for pinnum, pin in pins.items(): + switches.append(ArduinoSwitch(pinnum, pin, board)) + add_entities(switches) + + +class ArduinoSwitch(SwitchEntity): + """Representation of an Arduino switch.""" + + def __init__(self, pin, options, board): + """Initialize the Pin.""" + self._pin = pin + self._name = options[CONF_NAME] + self.pin_type = CONF_TYPE + self.direction = "out" + + self._state = options[CONF_INITIAL] + + if options[CONF_NEGATE]: + self.turn_on_handler = board.set_digital_out_low + self.turn_off_handler = board.set_digital_out_high + else: + self.turn_on_handler = board.set_digital_out_high + self.turn_off_handler = board.set_digital_out_low + + board.set_mode(self._pin, self.direction, self.pin_type) + (self.turn_on_handler if self._state else self.turn_off_handler)(pin) + + @property + def name(self): + """Get the name of the pin.""" + return self._name + + @property + def is_on(self): + """Return true if pin is high/on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the pin to high/on.""" + self._state = True + self.turn_on_handler(self._pin) + + def turn_off(self, **kwargs): + """Turn the pin to low/off.""" + self._state = False + self.turn_off_handler(self._pin) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arest/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/arest/__init__.py new file mode 100644 index 00000000000..37a104c08fe --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arest/__init__.py @@ -0,0 +1 @@ +"""The arest component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arest/binary_sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/arest/binary_sensor.py new file mode 100644 index 00000000000..3cd9038f1a8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arest/binary_sensor.py @@ -0,0 +1,122 @@ +"""Support for an exposed aREST RESTful API of a device.""" +from datetime import timedelta +import logging + +import requests +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES_SCHEMA, + PLATFORM_SCHEMA, + BinarySensorEntity, +) +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_NAME, + CONF_PIN, + CONF_RESOURCE, + HTTP_OK, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_RESOURCE): cv.url, + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_PIN): cv.string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the aREST binary sensor.""" + resource = config[CONF_RESOURCE] + pin = config[CONF_PIN] + device_class = config.get(CONF_DEVICE_CLASS) + + try: + response = requests.get(resource, timeout=10).json() + except requests.exceptions.MissingSchema: + _LOGGER.error( + "Missing resource or schema in configuration. Add http:// to your URL" + ) + return False + except requests.exceptions.ConnectionError: + _LOGGER.error("No route to device at %s", resource) + return False + + arest = ArestData(resource, pin) + + add_entities( + [ + ArestBinarySensor( + arest, + resource, + config.get(CONF_NAME, response[CONF_NAME]), + device_class, + pin, + ) + ], + True, + ) + + +class ArestBinarySensor(BinarySensorEntity): + """Implement an aREST binary sensor for a pin.""" + + def __init__(self, arest, resource, name, device_class, pin): + """Initialize the aREST device.""" + self.arest = arest + self._resource = resource + self._name = name + self._device_class = device_class + self._pin = pin + + if self._pin is not None: + request = requests.get(f"{self._resource}/mode/{self._pin}/i", timeout=10) + if request.status_code != HTTP_OK: + _LOGGER.error("Can't set mode of %s", self._resource) + + @property + def name(self): + """Return the name of the binary sensor.""" + return self._name + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return bool(self.arest.data.get("state")) + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._device_class + + def update(self): + """Get the latest data from aREST API.""" + self.arest.update() + + +class ArestData: + """Class for handling the data retrieval for pins.""" + + def __init__(self, resource, pin): + """Initialize the aREST data object.""" + self._resource = resource + self._pin = pin + self.data = {} + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from aREST device.""" + try: + response = requests.get(f"{self._resource}/digital/{self._pin}", timeout=10) + self.data = {"state": response.json()["return_value"]} + except requests.exceptions.ConnectionError: + _LOGGER.error("No route to device '%s'", self._resource) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arest/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arest/manifest.json new file mode 100644 index 00000000000..8a3b676c518 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arest/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "arest", + "name": "aREST", + "documentation": "https://www.home-assistant.io/integrations/arest", + "codeowners": ["@fabaff"], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arest/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/arest/sensor.py new file mode 100644 index 00000000000..061c15eafb0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arest/sensor.py @@ -0,0 +1,218 @@ +"""Support for an exposed aREST RESTful API of a device.""" +from datetime import timedelta +import logging + +import requests +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.const import ( + CONF_MONITORED_VARIABLES, + CONF_NAME, + CONF_RESOURCE, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, + HTTP_OK, +) +from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) + +CONF_FUNCTIONS = "functions" +CONF_PINS = "pins" + +DEFAULT_NAME = "aREST sensor" + +PIN_VARIABLE_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + } +) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_RESOURCE): cv.url, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PINS, default={}): vol.Schema( + {cv.string: PIN_VARIABLE_SCHEMA} + ), + vol.Optional(CONF_MONITORED_VARIABLES, default={}): vol.Schema( + {cv.string: PIN_VARIABLE_SCHEMA} + ), + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the aREST sensor.""" + resource = config[CONF_RESOURCE] + var_conf = config[CONF_MONITORED_VARIABLES] + pins = config[CONF_PINS] + + try: + response = requests.get(resource, timeout=10).json() + except requests.exceptions.MissingSchema: + _LOGGER.error( + "Missing resource or schema in configuration. Add http:// to your URL" + ) + return False + except requests.exceptions.ConnectionError: + _LOGGER.error("No route to device at %s", resource) + return False + + arest = ArestData(resource) + + def make_renderer(value_template): + """Create a renderer based on variable_template value.""" + if value_template is None: + return lambda value: value + + value_template.hass = hass + + def _render(value): + try: + return value_template.async_render({"value": value}, parse_result=False) + except TemplateError: + _LOGGER.exception("Error parsing value") + return value + + return _render + + dev = [] + + if var_conf is not None: + for variable, var_data in var_conf.items(): + if variable not in response["variables"]: + _LOGGER.error("Variable: %s does not exist", variable) + continue + + renderer = make_renderer(var_data.get(CONF_VALUE_TEMPLATE)) + dev.append( + ArestSensor( + arest, + resource, + config.get(CONF_NAME, response[CONF_NAME]), + var_data.get(CONF_NAME, variable), + variable=variable, + unit_of_measurement=var_data.get(CONF_UNIT_OF_MEASUREMENT), + renderer=renderer, + ) + ) + + if pins is not None: + for pinnum, pin in pins.items(): + renderer = make_renderer(pin.get(CONF_VALUE_TEMPLATE)) + dev.append( + ArestSensor( + ArestData(resource, pinnum), + resource, + config.get(CONF_NAME, response[CONF_NAME]), + pin.get(CONF_NAME), + pin=pinnum, + unit_of_measurement=pin.get(CONF_UNIT_OF_MEASUREMENT), + renderer=renderer, + ) + ) + + add_entities(dev, True) + + +class ArestSensor(SensorEntity): + """Implementation of an aREST sensor for exposed variables.""" + + def __init__( + self, + arest, + resource, + location, + name, + variable=None, + pin=None, + unit_of_measurement=None, + renderer=None, + ): + """Initialize the sensor.""" + self.arest = arest + self._resource = resource + self._name = f"{location.title()} {name.title()}" + self._variable = variable + self._pin = pin + self._state = None + self._unit_of_measurement = unit_of_measurement + self._renderer = renderer + + if self._pin is not None: + request = requests.get(f"{self._resource}/mode/{self._pin}/i", timeout=10) + if request.status_code != HTTP_OK: + _LOGGER.error("Can't set mode of %s", self._resource) + + @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 self._unit_of_measurement + + @property + def state(self): + """Return the state of the sensor.""" + values = self.arest.data + + if "error" in values: + return values["error"] + + value = self._renderer(values.get("value", values.get(self._variable, None))) + return value + + def update(self): + """Get the latest data from aREST API.""" + self.arest.update() + + @property + def available(self): + """Could the device be accessed during the last update call.""" + return self.arest.available + + +class ArestData: + """The Class for handling the data retrieval for variables.""" + + def __init__(self, resource, pin=None): + """Initialize the data object.""" + self._resource = resource + self._pin = pin + self.data = {} + self.available = True + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from aREST device.""" + try: + if self._pin is None: + response = requests.get(self._resource, timeout=10) + self.data = response.json()["variables"] + else: + try: + if str(self._pin[0]) == "A": + response = requests.get( + f"{self._resource}/analog/{self._pin[1:]}", timeout=10 + ) + self.data = {"value": response.json()["return_value"]} + except TypeError: + response = requests.get( + f"{self._resource}/digital/{self._pin}", timeout=10 + ) + self.data = {"value": response.json()["return_value"]} + self.available = True + except requests.exceptions.ConnectionError: + _LOGGER.error("No route to device %s", self._resource) + self.available = False diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arest/switch.py b/homeassistant-2021.6.0.dev0/homeassistant/components/arest/switch.py new file mode 100644 index 00000000000..ddd6b51f76d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arest/switch.py @@ -0,0 +1,210 @@ +"""Support for an exposed aREST RESTful API of a device.""" + +import logging + +import requests +import voluptuous as vol + +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.const import CONF_NAME, CONF_RESOURCE, HTTP_OK +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_FUNCTIONS = "functions" +CONF_PINS = "pins" +CONF_INVERT = "invert" + +DEFAULT_NAME = "aREST switch" + +PIN_FUNCTION_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_INVERT, default=False): cv.boolean, + } +) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_RESOURCE): cv.url, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PINS, default={}): vol.Schema( + {cv.string: PIN_FUNCTION_SCHEMA} + ), + vol.Optional(CONF_FUNCTIONS, default={}): vol.Schema( + {cv.string: PIN_FUNCTION_SCHEMA} + ), + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the aREST switches.""" + resource = config[CONF_RESOURCE] + + try: + response = requests.get(resource, timeout=10) + except requests.exceptions.MissingSchema: + _LOGGER.error( + "Missing resource or schema in configuration. Add http:// to your URL" + ) + return False + except requests.exceptions.ConnectionError: + _LOGGER.error("No route to device at %s", resource) + return False + + dev = [] + pins = config[CONF_PINS] + for pinnum, pin in pins.items(): + dev.append( + ArestSwitchPin( + resource, + config.get(CONF_NAME, response.json()[CONF_NAME]), + pin.get(CONF_NAME), + pinnum, + pin[CONF_INVERT], + ) + ) + + functions = config[CONF_FUNCTIONS] + for funcname, func in functions.items(): + dev.append( + ArestSwitchFunction( + resource, + config.get(CONF_NAME, response.json()[CONF_NAME]), + func.get(CONF_NAME), + funcname, + ) + ) + + add_entities(dev) + + +class ArestSwitchBase(SwitchEntity): + """Representation of an aREST switch.""" + + def __init__(self, resource, location, name): + """Initialize the switch.""" + self._resource = resource + self._name = f"{location.title()} {name.title()}" + self._state = None + self._available = True + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + @property + def available(self): + """Could the device be accessed during the last update call.""" + return self._available + + +class ArestSwitchFunction(ArestSwitchBase): + """Representation of an aREST switch.""" + + def __init__(self, resource, location, name, func): + """Initialize the switch.""" + super().__init__(resource, location, name) + self._func = func + + request = requests.get(f"{self._resource}/{self._func}", timeout=10) + + if request.status_code != HTTP_OK: + _LOGGER.error("Can't find function") + return + + try: + request.json()["return_value"] + except KeyError: + _LOGGER.error("No return_value received") + except ValueError: + _LOGGER.error("Response invalid") + + def turn_on(self, **kwargs): + """Turn the device on.""" + request = requests.get( + f"{self._resource}/{self._func}", timeout=10, params={"params": "1"} + ) + + if request.status_code == HTTP_OK: + self._state = True + else: + _LOGGER.error("Can't turn on function %s at %s", self._func, self._resource) + + def turn_off(self, **kwargs): + """Turn the device off.""" + request = requests.get( + f"{self._resource}/{self._func}", timeout=10, params={"params": "0"} + ) + + if request.status_code == HTTP_OK: + self._state = False + else: + _LOGGER.error( + "Can't turn off function %s at %s", self._func, self._resource + ) + + def update(self): + """Get the latest data from aREST API and update the state.""" + try: + request = requests.get(f"{self._resource}/{self._func}", timeout=10) + self._state = request.json()["return_value"] != 0 + self._available = True + except requests.exceptions.ConnectionError: + _LOGGER.warning("No route to device %s", self._resource) + self._available = False + + +class ArestSwitchPin(ArestSwitchBase): + """Representation of an aREST switch. Based on digital I/O.""" + + def __init__(self, resource, location, name, pin, invert): + """Initialize the switch.""" + super().__init__(resource, location, name) + self._pin = pin + self.invert = invert + + request = requests.get(f"{self._resource}/mode/{self._pin}/o", timeout=10) + if request.status_code != HTTP_OK: + _LOGGER.error("Can't set mode") + self._available = False + + def turn_on(self, **kwargs): + """Turn the device on.""" + turn_on_payload = int(not self.invert) + request = requests.get( + f"{self._resource}/digital/{self._pin}/{turn_on_payload}", timeout=10 + ) + if request.status_code == HTTP_OK: + self._state = True + else: + _LOGGER.error("Can't turn on pin %s at %s", self._pin, self._resource) + + def turn_off(self, **kwargs): + """Turn the device off.""" + turn_off_payload = int(self.invert) + request = requests.get( + f"{self._resource}/digital/{self._pin}/{turn_off_payload}", timeout=10 + ) + if request.status_code == HTTP_OK: + self._state = False + else: + _LOGGER.error("Can't turn off pin %s at %s", self._pin, self._resource) + + def update(self): + """Get the latest data from aREST API and update the state.""" + try: + request = requests.get(f"{self._resource}/digital/{self._pin}", timeout=10) + status_value = int(self.invert) + self._state = request.json()["return_value"] != status_value + self._available = True + except requests.exceptions.ConnectionError: + _LOGGER.warning("No route to device %s", self._resource) + self._available = False diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arlo/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/arlo/__init__.py new file mode 100644 index 00000000000..4338d3bab7e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arlo/__init__.py @@ -0,0 +1,87 @@ +"""Support for Netgear Arlo IP cameras.""" +from datetime import timedelta +import logging + +from pyarlo import PyArlo +from requests.exceptions import ConnectTimeout, HTTPError +import voluptuous as vol + +from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import track_time_interval + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = "Data provided by arlo.netgear.com" + +DATA_ARLO = "data_arlo" +DEFAULT_BRAND = "Netgear Arlo" +DOMAIN = "arlo" + +NOTIFICATION_ID = "arlo_notification" +NOTIFICATION_TITLE = "Arlo Component Setup" + +SCAN_INTERVAL = timedelta(seconds=60) + +SIGNAL_UPDATE_ARLO = "arlo_update" + +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=SCAN_INTERVAL): cv.time_period, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Set up an Arlo component.""" + conf = config[DOMAIN] + username = conf[CONF_USERNAME] + password = conf[CONF_PASSWORD] + scan_interval = conf[CONF_SCAN_INTERVAL] + + try: + + arlo = PyArlo(username, password, preload=False) + if not arlo.is_connected: + return False + + # assign refresh period to base station thread + arlo_base_station = next((station for station in arlo.base_stations), None) + + if arlo_base_station is not None: + arlo_base_station.refresh_rate = scan_interval.total_seconds() + elif not arlo.cameras: + _LOGGER.error("No Arlo camera or base station available") + return False + + hass.data[DATA_ARLO] = arlo + + except (ConnectTimeout, HTTPError) as ex: + _LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex)) + hass.components.persistent_notification.create( + f"Error: {ex}
You will need to restart hass after fixing.", + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID, + ) + return False + + def hub_refresh(event_time): + """Call ArloHub to refresh information.""" + _LOGGER.debug("Updating Arlo Hub component") + hass.data[DATA_ARLO].update(update_cameras=True, update_base_station=True) + dispatcher_send(hass, SIGNAL_UPDATE_ARLO) + + # register service + hass.services.register(DOMAIN, "update", hub_refresh) + + # register scan interval for ArloHub + track_time_interval(hass, hub_refresh, scan_interval) + return True diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arlo/alarm_control_panel.py b/homeassistant-2021.6.0.dev0/homeassistant/components/arlo/alarm_control_panel.py new file mode 100644 index 00000000000..dd899cbd04f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arlo/alarm_control_panel.py @@ -0,0 +1,158 @@ +"""Support for Arlo Alarm Control Panels.""" +import logging + +import voluptuous as vol + +from homeassistant.components.alarm_control_panel import ( + PLATFORM_SCHEMA, + AlarmControlPanelEntity, +) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) +from homeassistant.const import ( + ATTR_ATTRIBUTION, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, +) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import ATTRIBUTION, DATA_ARLO, SIGNAL_UPDATE_ARLO + +_LOGGER = logging.getLogger(__name__) + +ARMED = "armed" + +CONF_HOME_MODE_NAME = "home_mode_name" +CONF_AWAY_MODE_NAME = "away_mode_name" +CONF_NIGHT_MODE_NAME = "night_mode_name" + +DISARMED = "disarmed" + +ICON = "mdi:security" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string, + vol.Optional(CONF_AWAY_MODE_NAME, default=ARMED): cv.string, + vol.Optional(CONF_NIGHT_MODE_NAME, default=ARMED): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Arlo Alarm Control Panels.""" + arlo = hass.data[DATA_ARLO] + + if not arlo.base_stations: + return + + home_mode_name = config[CONF_HOME_MODE_NAME] + away_mode_name = config[CONF_AWAY_MODE_NAME] + night_mode_name = config[CONF_NIGHT_MODE_NAME] + base_stations = [] + for base_station in arlo.base_stations: + base_stations.append( + ArloBaseStation( + base_station, home_mode_name, away_mode_name, night_mode_name + ) + ) + add_entities(base_stations, True) + + +class ArloBaseStation(AlarmControlPanelEntity): + """Representation of an Arlo Alarm Control Panel.""" + + def __init__(self, data, home_mode_name, away_mode_name, night_mode_name): + """Initialize the alarm control panel.""" + self._base_station = data + self._home_mode_name = home_mode_name + self._away_mode_name = away_mode_name + self._night_mode_name = night_mode_name + self._state = None + + @property + def icon(self): + """Return icon.""" + return ICON + + async def async_added_to_hass(self): + """Register callbacks.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_ARLO, self._update_callback + ) + ) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + + def update(self): + """Update the state of the device.""" + _LOGGER.debug("Updating Arlo Alarm Control Panel %s", self.name) + mode = self._base_station.mode + if mode: + self._state = self._get_state_from_mode(mode) + else: + self._state = None + + def alarm_disarm(self, code=None): + """Send disarm command.""" + self._base_station.mode = DISARMED + + def alarm_arm_away(self, code=None): + """Send arm away command. Uses custom mode.""" + self._base_station.mode = self._away_mode_name + + def alarm_arm_home(self, code=None): + """Send arm home command. Uses custom mode.""" + self._base_station.mode = self._home_mode_name + + def alarm_arm_night(self, code=None): + """Send arm night command. Uses custom mode.""" + self._base_station.mode = self._night_mode_name + + @property + def name(self): + """Return the name of the base station.""" + return self._base_station.name + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: ATTRIBUTION, + "device_id": self._base_station.device_id, + } + + def _get_state_from_mode(self, mode): + """Convert Arlo mode to Home Assistant state.""" + if mode == ARMED: + return STATE_ALARM_ARMED_AWAY + if mode == DISARMED: + return STATE_ALARM_DISARMED + if mode == self._home_mode_name: + return STATE_ALARM_ARMED_HOME + if mode == self._away_mode_name: + return STATE_ALARM_ARMED_AWAY + if mode == self._night_mode_name: + return STATE_ALARM_ARMED_NIGHT + return mode diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arlo/camera.py b/homeassistant-2021.6.0.dev0/homeassistant/components/arlo/camera.py new file mode 100644 index 00000000000..c1848661429 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arlo/camera.py @@ -0,0 +1,167 @@ +"""Support for Netgear Arlo IP cameras.""" +import logging + +from haffmpeg.camera import CameraMjpeg +import voluptuous as vol + +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.const import ATTR_BATTERY_LEVEL +from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import DATA_ARLO, DEFAULT_BRAND, SIGNAL_UPDATE_ARLO + +_LOGGER = logging.getLogger(__name__) + +ARLO_MODE_ARMED = "armed" +ARLO_MODE_DISARMED = "disarmed" + +ATTR_BRIGHTNESS = "brightness" +ATTR_FLIPPED = "flipped" +ATTR_MIRRORED = "mirrored" +ATTR_MOTION = "motion_detection_sensitivity" +ATTR_POWERSAVE = "power_save_mode" +ATTR_SIGNAL_STRENGTH = "signal_strength" +ATTR_UNSEEN_VIDEOS = "unseen_videos" +ATTR_LAST_REFRESH = "last_refresh" + +CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" +DEFAULT_ARGUMENTS = "-pred 1" + +POWERSAVE_MODE_MAPPING = {1: "best_battery_life", 2: "optimized", 3: "best_video"} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string} +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up an Arlo IP Camera.""" + arlo = hass.data[DATA_ARLO] + + cameras = [] + for camera in arlo.cameras: + cameras.append(ArloCam(hass, camera, config)) + + add_entities(cameras) + + +class ArloCam(Camera): + """An implementation of a Netgear Arlo IP camera.""" + + def __init__(self, hass, camera, device_info): + """Initialize an Arlo camera.""" + super().__init__() + self._camera = camera + self._name = self._camera.name + self._motion_status = False + self._ffmpeg = hass.data[DATA_FFMPEG] + self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) + self._last_refresh = None + self.attrs = {} + + def camera_image(self): + """Return a still image response from the camera.""" + return self._camera.last_image_from_cache + + async def async_added_to_hass(self): + """Register callbacks.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_ARLO, self.async_write_ha_state + ) + ) + + async def handle_async_mjpeg_stream(self, request): + """Generate an HTTP MJPEG stream from the camera.""" + video = await self.hass.async_add_executor_job( + getattr, self._camera, "last_video" + ) + + if not video: + error_msg = ( + f"Video not found for {self.name}. " + f"Is it older than {self._camera.min_days_vdo_cache} days?" + ) + _LOGGER.error(error_msg) + return + + stream = CameraMjpeg(self._ffmpeg.binary) + await stream.open_camera(video.video_url, extra_cmd=self._ffmpeg_arguments) + + try: + stream_reader = await stream.get_reader() + return await async_aiohttp_proxy_stream( + self.hass, + request, + stream_reader, + self._ffmpeg.ffmpeg_stream_content_type, + ) + finally: + await stream.close() + + @property + def name(self): + """Return the name of this camera.""" + return self._name + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return { + name: value + for name, value in ( + (ATTR_BATTERY_LEVEL, self._camera.battery_level), + (ATTR_BRIGHTNESS, self._camera.brightness), + (ATTR_FLIPPED, self._camera.flip_state), + (ATTR_MIRRORED, self._camera.mirror_state), + (ATTR_MOTION, self._camera.motion_detection_sensitivity), + ( + ATTR_POWERSAVE, + POWERSAVE_MODE_MAPPING.get(self._camera.powersave_mode), + ), + (ATTR_SIGNAL_STRENGTH, self._camera.signal_strength), + (ATTR_UNSEEN_VIDEOS, self._camera.unseen_videos), + ) + if value is not None + } + + @property + def model(self): + """Return the camera model.""" + return self._camera.model_id + + @property + def brand(self): + """Return the camera brand.""" + return DEFAULT_BRAND + + @property + def motion_detection_enabled(self): + """Return the camera motion detection status.""" + return self._motion_status + + def set_base_station_mode(self, mode): + """Set the mode in the base station.""" + # Get the list of base stations identified by library + base_stations = self.hass.data[DATA_ARLO].base_stations + + # Some Arlo cameras does not have base station + # So check if there is base station detected first + # if yes, then choose the primary base station + # Set the mode on the chosen base station + if base_stations: + primary_base_station = base_stations[0] + primary_base_station.mode = mode + + def enable_motion_detection(self): + """Enable the Motion detection in base station (Arm).""" + self._motion_status = True + self.set_base_station_mode(ARLO_MODE_ARMED) + + def disable_motion_detection(self): + """Disable the motion detection in base station (Disarm).""" + self._motion_status = False + self.set_base_station_mode(ARLO_MODE_DISARMED) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arlo/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arlo/manifest.json new file mode 100644 index 00000000000..7b4978b56c1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arlo/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "arlo", + "name": "Arlo", + "documentation": "https://www.home-assistant.io/integrations/arlo", + "requirements": ["pyarlo==0.2.4"], + "dependencies": ["ffmpeg"], + "codeowners": [], + "iot_class": "cloud_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arlo/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/arlo/sensor.py new file mode 100644 index 00000000000..c794bf1ef5e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arlo/sensor.py @@ -0,0 +1,195 @@ +"""Sensor support for Netgear Arlo IP cameras.""" +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONCENTRATION_PARTS_PER_MILLION, + CONF_MONITORED_CONDITIONS, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, + TEMP_CELSIUS, +) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.icon import icon_for_battery_level + +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"], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ) + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up an Arlo IP sensor.""" + arlo = hass.data.get(DATA_ARLO) + if not arlo: + 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)) + else: + for camera in arlo.cameras: + if sensor_type in ("temperature", "humidity", "air_quality"): + continue + + name = f"{SENSOR_TYPES[sensor_type][0]} {camera.name}" + sensors.append(ArloSensor(name, camera, sensor_type)) + + for base_station in arlo.base_stations: + if ( + sensor_type 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)) + + add_entities(sensors, True) + + +class ArloSensor(SensorEntity): + """An implementation of a Netgear Arlo IP sensor.""" + + def __init__(self, name, device, sensor_type): + """Initialize an Arlo sensor.""" + _LOGGER.debug("ArloSensor created for %s", name) + self._name = name + 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.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_ARLO, self._update_callback + ) + ) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + if self._sensor_type == "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 + + 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": + self._state = len(self._data.cameras) + + elif self._sensor_type == "captured_today": + self._state = len(self._data.captured_today) + + elif self._sensor_type == "last_capture": + try: + video = self._data.last_video + self._state = video.created_at_pretty("%m-%d-%Y %H:%M:%S") + except (AttributeError, IndexError): + error_msg = ( + f"Video not found for {self.name}. " + f"Older than {self._data.min_days_vdo_cache} days?" + ) + _LOGGER.debug(error_msg) + self._state = None + + elif self._sensor_type == "battery_level": + try: + self._state = self._data.battery_level + except TypeError: + self._state = None + + elif self._sensor_type == "signal_strength": + try: + self._state = self._data.signal_strength + except TypeError: + self._state = None + + elif self._sensor_type == "temperature": + try: + self._state = self._data.ambient_temperature + except TypeError: + self._state = None + + elif self._sensor_type == "humidity": + try: + self._state = self._data.ambient_humidity + except TypeError: + self._state = None + + elif self._sensor_type == "air_quality": + try: + self._state = self._data.ambient_air_quality + except TypeError: + self._state = None + + @property + def extra_state_attributes(self): + """Return the device state attributes.""" + attrs = {} + + attrs[ATTR_ATTRIBUTION] = ATTRIBUTION + attrs["brand"] = DEFAULT_BRAND + + if self._sensor_type != "total_cameras": + attrs["model"] = self._data.model_id + + return attrs diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arlo/services.yaml b/homeassistant-2021.6.0.dev0/homeassistant/components/arlo/services.yaml new file mode 100644 index 00000000000..8481ffc4d53 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arlo/services.yaml @@ -0,0 +1,5 @@ +# Describes the format for available arlo services + +update: + name: Update + description: Update the state for all cameras and the base station. diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arris_tg2492lg/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/arris_tg2492lg/__init__.py new file mode 100644 index 00000000000..c08ddcba48f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arris_tg2492lg/__init__.py @@ -0,0 +1 @@ +"""The Arris TG2492LG component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arris_tg2492lg/device_tracker.py b/homeassistant-2021.6.0.dev0/homeassistant/components/arris_tg2492lg/device_tracker.py new file mode 100644 index 00000000000..1011d76f8aa --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arris_tg2492lg/device_tracker.py @@ -0,0 +1,67 @@ +"""Support for Arris TG2492LG router.""" +from __future__ import annotations + +from arris_tg2492lg import ConnectBox, Device +import voluptuous as vol + +from homeassistant.components.device_tracker import ( + DOMAIN, + PLATFORM_SCHEMA, + DeviceScanner, +) +from homeassistant.const import CONF_HOST, CONF_PASSWORD +import homeassistant.helpers.config_validation as cv + +DEFAULT_HOST = "192.168.178.1" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + } +) + + +def get_scanner(hass, config): + """Return the Arris device scanner.""" + conf = config[DOMAIN] + url = f"http://{conf[CONF_HOST]}" + connect_box = ConnectBox(url, conf[CONF_PASSWORD]) + return ArrisDeviceScanner(connect_box) + + +class ArrisDeviceScanner(DeviceScanner): + """This class queries a Arris TG2492LG router for connected devices.""" + + def __init__(self, connect_box: ConnectBox): + """Initialize the scanner.""" + self.connect_box = connect_box + self.last_results: list[Device] = [] + + def scan_devices(self): + """Scan for new devices and return a list with found device IDs.""" + self._update_info() + + 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.""" + name = next( + (result.hostname for result in self.last_results if result.mac == device), + None, + ) + return name + + def _update_info(self): + """Ensure the information from the Arris TG2492LG router is up to date.""" + result = self.connect_box.get_connected_devices() + + last_results = [] + mac_addresses = set() + + for device in result: + if device.online and device.mac not in mac_addresses: + last_results.append(device) + mac_addresses.add(device.mac) + + self.last_results = last_results diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arris_tg2492lg/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arris_tg2492lg/manifest.json new file mode 100644 index 00000000000..8ed5c39882e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arris_tg2492lg/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "arris_tg2492lg", + "name": "Arris TG2492LG", + "documentation": "https://www.home-assistant.io/integrations/arris_tg2492lg", + "requirements": ["arris-tg2492lg==1.1.0"], + "codeowners": ["@vanbalken"], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aruba/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aruba/__init__.py new file mode 100644 index 00000000000..cd52f7310f3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aruba/__init__.py @@ -0,0 +1 @@ +"""The aruba component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aruba/device_tracker.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aruba/device_tracker.py new file mode 100644 index 00000000000..355bcad3aaf --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aruba/device_tracker.py @@ -0,0 +1,135 @@ +"""Support for Aruba Access Points.""" +import logging +import re + +import pexpect +import voluptuous as vol + +from homeassistant.components.device_tracker import ( + DOMAIN, + PLATFORM_SCHEMA, + DeviceScanner, +) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +_DEVICES_REGEX = re.compile( + r"(?P([^\s]+)?)\s+" + + r"(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})\s+" + + r"(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))\s+" +) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + } +) + + +def get_scanner(hass, config): + """Validate the configuration and return a Aruba scanner.""" + scanner = ArubaDeviceScanner(config[DOMAIN]) + + return scanner if scanner.success_init else None + + +class ArubaDeviceScanner(DeviceScanner): + """This class queries a Aruba Access Point for connected devices.""" + + def __init__(self, config): + """Initialize the scanner.""" + self.host = config[CONF_HOST] + self.username = config[CONF_USERNAME] + self.password = config[CONF_PASSWORD] + + self.last_results = {} + + # Test the router is accessible. + data = self.get_aruba_data() + self.success_init = data is not None + + def scan_devices(self): + """Scan for new devices and return a list with found device IDs.""" + self._update_info() + return [client["mac"] for client in self.last_results] + + def get_device_name(self, device): + """Return the name of the given device or None if we don't know.""" + if not self.last_results: + return None + for client in self.last_results: + if client["mac"] == device: + return client["name"] + return None + + def _update_info(self): + """Ensure the information from the Aruba Access Point is up to date. + + Return boolean if scanning successful. + """ + if not self.success_init: + return False + + data = self.get_aruba_data() + if not data: + return False + + self.last_results = data.values() + return True + + def get_aruba_data(self): + """Retrieve data from Aruba Access Point and return parsed result.""" + + connect = f"ssh {self.username}@{self.host}" + ssh = pexpect.spawn(connect) + query = ssh.expect( + [ + "password:", + pexpect.TIMEOUT, + pexpect.EOF, + "continue connecting (yes/no)?", + "Host key verification failed.", + "Connection refused", + "Connection timed out", + ], + timeout=120, + ) + if query == 1: + _LOGGER.error("Timeout") + return + if query == 2: + _LOGGER.error("Unexpected response from router") + return + if query == 3: + ssh.sendline("yes") + ssh.expect("password:") + elif query == 4: + _LOGGER.error("Host key changed") + return + elif query == 5: + _LOGGER.error("Connection refused by server") + return + elif query == 6: + _LOGGER.error("Connection timed out") + return + ssh.sendline(self.password) + ssh.expect("#") + ssh.sendline("show clients") + ssh.expect("#") + devices_result = ssh.before.split(b"\r\n") + ssh.sendline("exit") + + devices = {} + for device in devices_result: + match = _DEVICES_REGEX.search(device.decode("utf-8")) + if match: + devices[match.group("ip")] = { + "ip": match.group("ip"), + "mac": match.group("mac").upper(), + "name": match.group("name"), + } + return devices diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aruba/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aruba/manifest.json new file mode 100644 index 00000000000..660ba9f06f1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aruba/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "aruba", + "name": "Aruba", + "documentation": "https://www.home-assistant.io/integrations/aruba", + "requirements": ["pexpect==4.6.0"], + "codeowners": [], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arwn/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/arwn/__init__.py new file mode 100644 index 00000000000..726f3dba5de --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arwn/__init__.py @@ -0,0 +1 @@ +"""The arwn component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arwn/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/arwn/manifest.json new file mode 100644 index 00000000000..b9781fd6aa7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arwn/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "arwn", + "name": "Ambient Radio Weather Network", + "documentation": "https://www.home-assistant.io/integrations/arwn", + "dependencies": ["mqtt"], + "codeowners": [], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/arwn/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/arwn/sensor.py new file mode 100644 index 00000000000..ba9166d1af5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/arwn/sensor.py @@ -0,0 +1,174 @@ +"""Support for collecting data from the ARWN project.""" +import json +import logging + +from homeassistant.components import mqtt +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import DEGREE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import callback +from homeassistant.util import slugify + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "arwn" + +DATA_ARWN = "arwn" +TOPIC = "arwn/#" + + +def discover_sensors(topic, payload): + """Given a topic, dynamically create the right sensor type. + + Async friendly. + """ + parts = topic.split("/") + unit = payload.get("units", "") + domain = parts[1] + if domain == "temperature": + name = parts[2] + if unit == "F": + unit = TEMP_FAHRENHEIT + else: + unit = TEMP_CELSIUS + return ArwnSensor(topic, name, "temp", unit) + if domain == "moisture": + name = f"{parts[2]} Moisture" + return ArwnSensor(topic, name, "moisture", unit, "mdi:water-percent") + if domain == "rain": + if len(parts) >= 3 and parts[2] == "today": + return ArwnSensor( + topic, "Rain Since Midnight", "since_midnight", "in", "mdi:water" + ) + return ( + ArwnSensor(topic + "/total", "Total Rainfall", "total", unit, "mdi:water"), + ArwnSensor(topic + "/rate", "Rainfall Rate", "rate", unit, "mdi:water"), + ) + if domain == "barometer": + return ArwnSensor(topic, "Barometer", "pressure", unit, "mdi:thermometer-lines") + if domain == "wind": + return ( + ArwnSensor( + topic + "/speed", "Wind Speed", "speed", unit, "mdi:speedometer" + ), + ArwnSensor(topic + "/gust", "Wind Gust", "gust", unit, "mdi:speedometer"), + ArwnSensor( + topic + "/dir", "Wind Direction", "direction", DEGREE, "mdi:compass" + ), + ) + + +def _slug(name): + return f"sensor.arwn_{slugify(name)}" + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the ARWN platform.""" + + @callback + def async_sensor_event_received(msg): + """Process events as sensors. + + When a new event on our topic (arwn/#) is received we map it + into a known kind of sensor based on topic name. If we've + never seen this before, we keep this sensor around in a global + cache. If we have seen it before, we update the values of the + existing sensor. Either way, we push an ha state update at the + end for the new event we've seen. + + This lets us dynamically incorporate sensors without any + configuration on our side. + """ + event = json.loads(msg.payload) + sensors = discover_sensors(msg.topic, event) + if not sensors: + return + + store = hass.data.get(DATA_ARWN) + if store is None: + store = hass.data[DATA_ARWN] = {} + + if isinstance(sensors, ArwnSensor): + sensors = (sensors,) + + if "timestamp" in event: + del event["timestamp"] + + for sensor in sensors: + if sensor.name not in store: + sensor.hass = hass + sensor.set_event(event) + store[sensor.name] = sensor + _LOGGER.debug( + "Registering sensor %(name)s => %(event)s", + {"name": sensor.name, "event": event}, + ) + async_add_entities((sensor,), True) + else: + _LOGGER.debug( + "Recording sensor %(name)s => %(event)s", + {"name": sensor.name, "event": event}, + ) + store[sensor.name].set_event(event) + + await mqtt.async_subscribe(hass, TOPIC, async_sensor_event_received, 0) + return True + + +class ArwnSensor(SensorEntity): + """Representation of an ARWN sensor.""" + + def __init__(self, topic, name, state_key, units, icon=None): + """Initialize the sensor.""" + self.hass = None + self.entity_id = _slug(name) + self._name = name + # This mqtt topic for the sensor which is its uid + self._uid = topic + self._state_key = state_key + self.event = {} + self._unit_of_measurement = units + self._icon = icon + + def set_event(self, event): + """Update the sensor with the most recent event.""" + self.event = {} + self.event.update(event) + self.async_write_ha_state() + + @property + def state(self): + """Return the state of the device.""" + return self.event.get(self._state_key, None) + + @property + def name(self): + """Get the name of the sensor.""" + return self._name + + @property + def unique_id(self): + """Return a unique ID. + + This is based on the topic that comes from mqtt + """ + return self._uid + + @property + def extra_state_attributes(self): + """Return all the state attributes.""" + return self.event + + @property + def unit_of_measurement(self): + """Return the unit of measurement the state is expressed in.""" + return self._unit_of_measurement + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def icon(self): + """Return the icon of device based on its type.""" + return self._icon diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_cdr/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_cdr/__init__.py new file mode 100644 index 00000000000..d681a392c56 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_cdr/__init__.py @@ -0,0 +1 @@ +"""The asterisk_cdr component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_cdr/mailbox.py b/homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_cdr/mailbox.py new file mode 100644 index 00000000000..28f48c7ab70 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_cdr/mailbox.py @@ -0,0 +1,61 @@ +"""Support for the Asterisk CDR interface.""" +import datetime +import hashlib + +from homeassistant.components.asterisk_mbox import ( + DOMAIN as ASTERISK_DOMAIN, + SIGNAL_CDR_UPDATE, +) +from homeassistant.components.mailbox import Mailbox +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +MAILBOX_NAME = "asterisk_cdr" + + +async def async_get_handler(hass, config, discovery_info=None): + """Set up the Asterix CDR platform.""" + return AsteriskCDR(hass, MAILBOX_NAME) + + +class AsteriskCDR(Mailbox): + """Asterisk VM Call Data Record mailbox.""" + + def __init__(self, hass, name): + """Initialize Asterisk CDR.""" + super().__init__(hass, name) + self.cdr = [] + async_dispatcher_connect(self.hass, SIGNAL_CDR_UPDATE, self._update_callback) + + @callback + def _update_callback(self, msg): + """Update the message count in HA, if needed.""" + self._build_message() + self.async_update() + + def _build_message(self): + """Build message structure.""" + cdr = [] + for entry in self.hass.data[ASTERISK_DOMAIN].cdr: + timestamp = datetime.datetime.strptime( + entry["time"], "%Y-%m-%d %H:%M:%S" + ).timestamp() + info = { + "origtime": timestamp, + "callerid": entry["callerid"], + "duration": entry["duration"], + } + sha = hashlib.sha256(str(entry).encode("utf-8")).hexdigest() + msg = ( + f"Destination: {entry['dest']}\n" + f"Application: {entry['application']}\n " + f"Context: {entry['context']}" + ) + cdr.append({"info": info, "sha": sha, "text": msg}) + self.cdr = cdr + + async def async_get_messages(self): + """Return a list of the current messages.""" + if not self.cdr: + self._build_message() + return self.cdr diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_cdr/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_cdr/manifest.json new file mode 100644 index 00000000000..c92d415fbee --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_cdr/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "asterisk_cdr", + "name": "Asterisk Call Detail Records", + "documentation": "https://www.home-assistant.io/integrations/asterisk_cdr", + "dependencies": ["asterisk_mbox"], + "codeowners": [], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_mbox/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_mbox/__init__.py new file mode 100644 index 00000000000..ca511267302 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_mbox/__init__.py @@ -0,0 +1,123 @@ +"""Support for Asterisk Voicemail interface.""" +import logging + +from asterisk_mbox import Client as asteriskClient +from asterisk_mbox.commands import ( + CMD_MESSAGE_CDR, + CMD_MESSAGE_CDR_AVAILABLE, + CMD_MESSAGE_LIST, +) +import voluptuous as vol + +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT +from homeassistant.core import callback +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_connect + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "asterisk_mbox" + +SIGNAL_DISCOVER_PLATFORM = "asterisk_mbox.discover_platform" +SIGNAL_MESSAGE_REQUEST = "asterisk_mbox.message_request" +SIGNAL_MESSAGE_UPDATE = "asterisk_mbox.message_updated" +SIGNAL_CDR_UPDATE = "asterisk_mbox.message_updated" +SIGNAL_CDR_REQUEST = "asterisk_mbox.message_request" + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_PORT): cv.port, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Set up for the Asterisk Voicemail box.""" + conf = config.get(DOMAIN) + + host = conf[CONF_HOST] + port = conf[CONF_PORT] + password = conf[CONF_PASSWORD] + + hass.data[DOMAIN] = AsteriskData(hass, host, port, password, config) + + return True + + +class AsteriskData: + """Store Asterisk mailbox data.""" + + def __init__(self, hass, host, port, password, config): + """Init the Asterisk data object.""" + + self.hass = hass + self.config = config + self.messages = None + self.cdr = None + + dispatcher_connect(self.hass, SIGNAL_MESSAGE_REQUEST, self._request_messages) + dispatcher_connect(self.hass, SIGNAL_CDR_REQUEST, self._request_cdr) + dispatcher_connect(self.hass, SIGNAL_DISCOVER_PLATFORM, self._discover_platform) + # Only connect after signal connection to ensure we don't miss any + self.client = asteriskClient(host, port, password, self.handle_data) + + @callback + def _discover_platform(self, component): + _LOGGER.debug("Adding mailbox %s", component) + self.hass.async_create_task( + discovery.async_load_platform( + self.hass, "mailbox", component, {}, self.config + ) + ) + + @callback + def handle_data(self, command, msg): + """Handle changes to the mailbox.""" + + if command == CMD_MESSAGE_LIST: + _LOGGER.debug("AsteriskVM sent updated message list: Len %d", len(msg)) + old_messages = self.messages + self.messages = sorted( + msg, key=lambda item: item["info"]["origtime"], reverse=True + ) + if not isinstance(old_messages, list): + async_dispatcher_send(self.hass, SIGNAL_DISCOVER_PLATFORM, DOMAIN) + async_dispatcher_send(self.hass, SIGNAL_MESSAGE_UPDATE, self.messages) + elif command == CMD_MESSAGE_CDR: + _LOGGER.debug( + "AsteriskVM sent updated CDR list: Len %d", len(msg.get("entries", [])) + ) + self.cdr = msg["entries"] + async_dispatcher_send(self.hass, SIGNAL_CDR_UPDATE, self.cdr) + elif command == CMD_MESSAGE_CDR_AVAILABLE: + if not isinstance(self.cdr, list): + _LOGGER.debug("AsteriskVM adding CDR platform") + self.cdr = [] + async_dispatcher_send( + self.hass, SIGNAL_DISCOVER_PLATFORM, "asterisk_cdr" + ) + async_dispatcher_send(self.hass, SIGNAL_CDR_REQUEST) + else: + _LOGGER.debug( + "AsteriskVM sent unknown message '%d' len: %d", command, len(msg) + ) + + @callback + def _request_messages(self): + """Handle changes to the mailbox.""" + _LOGGER.debug("Requesting message list") + self.client.messages() + + @callback + def _request_cdr(self): + """Handle changes to the CDR.""" + _LOGGER.debug("Requesting CDR list") + self.client.get_cdr() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_mbox/mailbox.py b/homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_mbox/mailbox.py new file mode 100644 index 00000000000..62d817df9a3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_mbox/mailbox.py @@ -0,0 +1,74 @@ +"""Support for the Asterisk Voicemail interface.""" +from functools import partial +import logging + +from asterisk_mbox import ServerError + +from homeassistant.components.mailbox import CONTENT_TYPE_MPEG, Mailbox, StreamError +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import DOMAIN as ASTERISK_DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SIGNAL_MESSAGE_REQUEST = "asterisk_mbox.message_request" +SIGNAL_MESSAGE_UPDATE = "asterisk_mbox.message_updated" + + +async def async_get_handler(hass, config, discovery_info=None): + """Set up the Asterix VM platform.""" + return AsteriskMailbox(hass, ASTERISK_DOMAIN) + + +class AsteriskMailbox(Mailbox): + """Asterisk VM Sensor.""" + + def __init__(self, hass, name): + """Initialize Asterisk mailbox.""" + super().__init__(hass, name) + async_dispatcher_connect( + self.hass, SIGNAL_MESSAGE_UPDATE, self._update_callback + ) + + @callback + def _update_callback(self, msg): + """Update the message count in HA, if needed.""" + self.async_update() + + @property + def media_type(self): + """Return the supported media type.""" + return CONTENT_TYPE_MPEG + + @property + def can_delete(self): + """Return if messages can be deleted.""" + return True + + @property + def has_media(self): + """Return if messages have attached media files.""" + return True + + async def async_get_media(self, msgid): + """Return the media blob for the msgid.""" + + client = self.hass.data[ASTERISK_DOMAIN].client + try: + return await self.hass.async_add_executor_job( + partial(client.mp3, msgid, sync=True) + ) + except ServerError as err: + raise StreamError(err) from err + + async def async_get_messages(self): + """Return a list of the current messages.""" + return self.hass.data[ASTERISK_DOMAIN].messages + + async def async_delete(self, msgid): + """Delete the specified messages.""" + client = self.hass.data[ASTERISK_DOMAIN].client + _LOGGER.info("Deleting: %s", msgid) + await self.hass.async_add_executor_job(client.delete, msgid) + return True diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_mbox/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_mbox/manifest.json new file mode 100644 index 00000000000..068da7d64f4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asterisk_mbox/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "asterisk_mbox", + "name": "Asterisk Voicemail", + "documentation": "https://www.home-assistant.io/integrations/asterisk_mbox", + "requirements": ["asterisk_mbox==0.5.0"], + "codeowners": [], + "iot_class": "local_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/__init__.py new file mode 100644 index 00000000000..ad3cea1106b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/__init__.py @@ -0,0 +1,164 @@ +"""Support for ASUSWRT devices.""" + +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_MODE, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_SENSORS, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv + +from .const import ( + CONF_DNSMASQ, + CONF_INTERFACE, + CONF_REQUIRE_IP, + CONF_SSH_KEY, + DATA_ASUSWRT, + DEFAULT_DNSMASQ, + DEFAULT_INTERFACE, + DEFAULT_SSH_PORT, + DOMAIN, + MODE_AP, + MODE_ROUTER, + PROTOCOL_SSH, + PROTOCOL_TELNET, +) +from .router import AsusWrtRouter + +PLATFORMS = ["device_tracker", "sensor"] + +CONF_PUB_KEY = "pub_key" +SECRET_GROUP = "Password or SSH Key" +SENSOR_TYPES = ["devices", "upload_speed", "download_speed", "download", "upload"] + +CONFIG_SCHEMA = vol.Schema( + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_PROTOCOL, default=PROTOCOL_SSH): vol.In( + [PROTOCOL_SSH, PROTOCOL_TELNET] + ), + vol.Optional(CONF_MODE, default=MODE_ROUTER): vol.In( + [MODE_ROUTER, MODE_AP] + ), + vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port, + vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean, + vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string, + vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile, + vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile, + vol.Optional(CONF_SENSORS): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ), + vol.Optional(CONF_INTERFACE, default=DEFAULT_INTERFACE): cv.string, + vol.Optional(CONF_DNSMASQ, default=DEFAULT_DNSMASQ): cv.string, + } + ) + }, + ), + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the AsusWrt integration.""" + conf = config.get(DOMAIN) + if conf is None: + return True + + # save the options from config yaml + options = {} + mode = conf.get(CONF_MODE, MODE_ROUTER) + for name, value in conf.items(): + if name in ([CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP]): + if name == CONF_REQUIRE_IP and mode != MODE_AP: + continue + options[name] = value + hass.data[DOMAIN] = {"yaml_options": options} + + # check if already configured + domains_list = hass.config_entries.async_domains() + if DOMAIN in domains_list: + return True + + # remove not required config keys + pub_key = conf.pop(CONF_PUB_KEY, "") + if pub_key: + conf[CONF_SSH_KEY] = pub_key + + conf.pop(CONF_REQUIRE_IP, True) + conf.pop(CONF_SENSORS, {}) + conf.pop(CONF_INTERFACE, "") + conf.pop(CONF_DNSMASQ, "") + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up AsusWrt platform.""" + + # import options from yaml if empty + yaml_options = hass.data.get(DOMAIN, {}).pop("yaml_options", {}) + if not entry.options and yaml_options: + hass.config_entries.async_update_entry(entry, options=yaml_options) + + router = AsusWrtRouter(hass, entry) + await router.setup() + + router.async_on_close(entry.add_update_listener(update_listener)) + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + async def async_close_connection(event): + """Close AsusWrt connection on HA Stop.""" + await router.close() + + stop_listener = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, async_close_connection + ) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + DATA_ASUSWRT: router, + "stop_listener": stop_listener, + } + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + if unload_ok: + hass.data[DOMAIN][entry.entry_id]["stop_listener"]() + router = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT] + await router.close() + + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Update when config_entry options update.""" + router = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT] + + if router.update_options(entry.options): + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/config_flow.py new file mode 100644 index 00000000000..8028a703ac0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/config_flow.py @@ -0,0 +1,236 @@ +"""Config flow to configure the AsusWrt integration.""" +import logging +import os +import socket + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.device_tracker.const import ( + CONF_CONSIDER_HOME, + DEFAULT_CONSIDER_HOME, +) +from homeassistant.const import ( + CONF_HOST, + CONF_MODE, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_USERNAME, +) +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv + +from .const import ( + CONF_DNSMASQ, + CONF_INTERFACE, + CONF_REQUIRE_IP, + CONF_SSH_KEY, + CONF_TRACK_UNKNOWN, + DEFAULT_DNSMASQ, + DEFAULT_INTERFACE, + DEFAULT_SSH_PORT, + DEFAULT_TRACK_UNKNOWN, + DOMAIN, + MODE_AP, + MODE_ROUTER, + PROTOCOL_SSH, + PROTOCOL_TELNET, +) +from .router import get_api + +RESULT_CONN_ERROR = "cannot_connect" +RESULT_UNKNOWN = "unknown" +RESULT_SUCCESS = "success" + +_LOGGER = logging.getLogger(__name__) + + +def _is_file(value) -> bool: + """Validate that the value is an existing file.""" + file_in = os.path.expanduser(str(value)) + + if not os.path.isfile(file_in): + return False + if not os.access(file_in, os.R_OK): + return False + return True + + +def _get_ip(host): + """Get the ip address from the host name.""" + try: + return socket.gethostbyname(host) + except socket.gaierror: + return None + + +class AsusWrtFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + + def __init__(self): + """Initialize AsusWrt config flow.""" + self._host = None + + @callback + def _show_setup_form(self, user_input=None, errors=None): + """Show the setup form to the user.""" + + if user_input is None: + user_input = {} + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str, + vol.Required( + CONF_USERNAME, default=user_input.get(CONF_USERNAME, "") + ): str, + vol.Optional(CONF_PASSWORD): str, + vol.Optional(CONF_SSH_KEY): str, + vol.Required(CONF_PROTOCOL, default=PROTOCOL_SSH): vol.In( + {PROTOCOL_SSH: "SSH", PROTOCOL_TELNET: "Telnet"} + ), + vol.Required(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port, + vol.Required(CONF_MODE, default=MODE_ROUTER): vol.In( + {MODE_ROUTER: "Router", MODE_AP: "Access Point"} + ), + } + ), + errors=errors or {}, + ) + + async def _async_check_connection(self, user_input): + """Attempt to connect the AsusWrt router.""" + + api = get_api(user_input) + try: + await api.connection.async_connect() + + except OSError: + _LOGGER.error("Error connecting to the AsusWrt router at %s", self._host) + return RESULT_CONN_ERROR + + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Unknown error connecting with AsusWrt router at %s", self._host + ) + return RESULT_UNKNOWN + + if not api.is_connected: + _LOGGER.error("Error connecting to the AsusWrt router at %s", self._host) + return RESULT_CONN_ERROR + + conf_protocol = user_input[CONF_PROTOCOL] + if conf_protocol == PROTOCOL_TELNET: + api.connection.disconnect() + return RESULT_SUCCESS + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if user_input is None: + return self._show_setup_form(user_input) + + errors = {} + self._host = user_input[CONF_HOST] + pwd = user_input.get(CONF_PASSWORD) + ssh = user_input.get(CONF_SSH_KEY) + + if not (pwd or ssh): + errors["base"] = "pwd_or_ssh" + elif ssh: + if pwd: + errors["base"] = "pwd_and_ssh" + else: + isfile = await self.hass.async_add_executor_job(_is_file, ssh) + if not isfile: + errors["base"] = "ssh_not_file" + + if not errors: + ip_address = await self.hass.async_add_executor_job(_get_ip, self._host) + if not ip_address: + errors["base"] = "invalid_host" + + if not errors: + result = await self._async_check_connection(user_input) + if result != RESULT_SUCCESS: + errors["base"] = result + + if errors: + return self._show_setup_form(user_input, errors) + + return self.async_create_entry( + title=self._host, + data=user_input, + ) + + async def async_step_import(self, user_input=None): + """Import a config entry.""" + return await self.async_step_user(user_input) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for AsusWrt.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + data_schema = vol.Schema( + { + vol.Optional( + CONF_CONSIDER_HOME, + default=self.config_entry.options.get( + CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds() + ), + ): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=900)), + vol.Optional( + CONF_TRACK_UNKNOWN, + default=self.config_entry.options.get( + CONF_TRACK_UNKNOWN, DEFAULT_TRACK_UNKNOWN + ), + ): bool, + vol.Required( + CONF_INTERFACE, + default=self.config_entry.options.get( + CONF_INTERFACE, DEFAULT_INTERFACE + ), + ): str, + vol.Required( + CONF_DNSMASQ, + default=self.config_entry.options.get( + CONF_DNSMASQ, DEFAULT_DNSMASQ + ), + ): str, + } + ) + + conf_mode = self.config_entry.data[CONF_MODE] + if conf_mode == MODE_AP: + data_schema = data_schema.extend( + { + vol.Optional( + CONF_REQUIRE_IP, + default=self.config_entry.options.get(CONF_REQUIRE_IP, True), + ): bool, + } + ) + + return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/const.py new file mode 100644 index 00000000000..a8977a77ea8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/const.py @@ -0,0 +1,28 @@ +"""AsusWrt component constants.""" +DOMAIN = "asuswrt" + +CONF_DNSMASQ = "dnsmasq" +CONF_INTERFACE = "interface" +CONF_REQUIRE_IP = "require_ip" +CONF_SSH_KEY = "ssh_key" +CONF_TRACK_UNKNOWN = "track_unknown" + +DATA_ASUSWRT = DOMAIN + +DEFAULT_DNSMASQ = "/var/lib/misc" +DEFAULT_INTERFACE = "eth0" +DEFAULT_SSH_PORT = 22 +DEFAULT_TRACK_UNKNOWN = False + +MODE_AP = "ap" +MODE_ROUTER = "router" + +PROTOCOL_SSH = "ssh" +PROTOCOL_TELNET = "telnet" + +# Sensors +SENSOR_CONNECTED_DEVICE = "sensor_connected_device" +SENSOR_RX_BYTES = "sensor_rx_bytes" +SENSOR_TX_BYTES = "sensor_tx_bytes" +SENSOR_RX_RATES = "sensor_rx_rates" +SENSOR_TX_RATES = "sensor_tx_rates" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/device_tracker.py b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/device_tracker.py new file mode 100644 index 00000000000..a0c7ec0e27a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/device_tracker.py @@ -0,0 +1,138 @@ +"""Support for ASUSWRT routers.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo + +from .const import DATA_ASUSWRT, DOMAIN +from .router import AsusWrtRouter + +DEFAULT_DEVICE_NAME = "Unknown device" + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +) -> None: + """Set up device tracker for AsusWrt component.""" + router = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT] + tracked = set() + + @callback + def update_router(): + """Update the values of the router.""" + add_entities(router, async_add_entities, tracked) + + router.async_on_close( + async_dispatcher_connect(hass, router.signal_device_new, update_router) + ) + + update_router() + + +@callback +def add_entities(router, async_add_entities, tracked): + """Add new tracker entities from the router.""" + new_tracked = [] + + for mac, device in router.devices.items(): + if mac in tracked: + continue + + new_tracked.append(AsusWrtDevice(router, device)) + tracked.add(mac) + + if new_tracked: + async_add_entities(new_tracked) + + +class AsusWrtDevice(ScannerEntity): + """Representation of a AsusWrt device.""" + + def __init__(self, router: AsusWrtRouter, device) -> None: + """Initialize a AsusWrt device.""" + self._router = router + self._device = device + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._device.mac + + @property + def name(self) -> str: + """Return the name.""" + return self._device.name or DEFAULT_DEVICE_NAME + + @property + def is_connected(self): + """Return true if the device is connected to the network.""" + return self._device.is_connected + + @property + def source_type(self) -> str: + """Return the source type.""" + return SOURCE_TYPE_ROUTER + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the attributes.""" + attrs = {} + if self._device.last_activity: + attrs["last_time_reachable"] = self._device.last_activity.isoformat( + timespec="seconds" + ) + return attrs + + @property + def hostname(self) -> str: + """Return the hostname of device.""" + return self._device.name + + @property + def ip_address(self) -> str: + """Return the primary ip address of the device.""" + return self._device.ip_address + + @property + def mac_address(self) -> str: + """Return the mac address of the device.""" + return self._device.mac + + @property + def device_info(self) -> DeviceInfo: + """Return the device information.""" + data = { + "connections": {(CONNECTION_NETWORK_MAC, self._device.mac)}, + } + if self._device.name: + data["default_name"] = self._device.name + + return data + + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False + + @callback + def async_on_demand_update(self): + """Update state.""" + self._device = self._router.devices[self._device.mac] + 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, + self._router.signal_device_update, + self.async_on_demand_update, + ) + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/manifest.json new file mode 100644 index 00000000000..b66c3bb5db9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "asuswrt", + "name": "ASUSWRT", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/asuswrt", + "requirements": ["aioasuswrt==1.3.4"], + "codeowners": ["@kennedyshead", "@ollo69"], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/router.py b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/router.py new file mode 100644 index 00000000000..f82bf74e4a3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/router.py @@ -0,0 +1,426 @@ +"""Represent the AsusWrt router.""" +from __future__ import annotations + +from datetime import datetime, timedelta +import logging +from typing import Any + +from aioasuswrt.asuswrt import AsusWrt + +from homeassistant.components.device_tracker.const import ( + CONF_CONSIDER_HOME, + DEFAULT_CONSIDER_HOME, + DOMAIN as TRACKER_DOMAIN, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_MODE, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_USERNAME, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt as dt_util + +from .const import ( + CONF_DNSMASQ, + CONF_INTERFACE, + CONF_REQUIRE_IP, + CONF_SSH_KEY, + CONF_TRACK_UNKNOWN, + DEFAULT_DNSMASQ, + DEFAULT_INTERFACE, + DEFAULT_TRACK_UNKNOWN, + DOMAIN, + PROTOCOL_TELNET, + SENSOR_CONNECTED_DEVICE, + SENSOR_RX_BYTES, + SENSOR_RX_RATES, + SENSOR_TX_BYTES, + SENSOR_TX_RATES, +) + +CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP] + +KEY_COORDINATOR = "coordinator" +KEY_SENSORS = "sensors" + +SCAN_INTERVAL = timedelta(seconds=30) + +SENSORS_TYPE_BYTES = "sensors_bytes" +SENSORS_TYPE_COUNT = "sensors_count" +SENSORS_TYPE_RATES = "sensors_rates" + +_LOGGER = logging.getLogger(__name__) + + +class AsusWrtSensorDataHandler: + """Data handler for AsusWrt sensor.""" + + def __init__(self, hass, api): + """Initialize a AsusWrt sensor data handler.""" + self._hass = hass + self._api = api + self._connected_devices = 0 + + async def _get_connected_devices(self): + """Return number of connected devices.""" + return {SENSOR_CONNECTED_DEVICE: self._connected_devices} + + async def _get_bytes(self): + """Fetch byte information from the router.""" + ret_dict: dict[str, Any] = {} + try: + datas = await self._api.async_get_bytes_total() + except OSError as exc: + raise UpdateFailed from exc + + ret_dict[SENSOR_RX_BYTES] = datas[0] + ret_dict[SENSOR_TX_BYTES] = datas[1] + + return ret_dict + + async def _get_rates(self): + """Fetch rates information from the router.""" + ret_dict: dict[str, Any] = {} + try: + rates = await self._api.async_get_current_transfer_rates() + except OSError as exc: + raise UpdateFailed from exc + + ret_dict[SENSOR_RX_RATES] = rates[0] + ret_dict[SENSOR_TX_RATES] = rates[1] + + return ret_dict + + def update_device_count(self, conn_devices: int): + """Update connected devices attribute.""" + if self._connected_devices == conn_devices: + return False + self._connected_devices = conn_devices + return True + + async def get_coordinator(self, sensor_type: str, should_poll=True): + """Get the coordinator for a specific sensor type.""" + if sensor_type == SENSORS_TYPE_COUNT: + method = self._get_connected_devices + elif sensor_type == SENSORS_TYPE_BYTES: + method = self._get_bytes + elif sensor_type == SENSORS_TYPE_RATES: + method = self._get_rates + else: + raise RuntimeError(f"Invalid sensor type: {sensor_type}") + + coordinator = DataUpdateCoordinator( + self._hass, + _LOGGER, + name=sensor_type, + update_method=method, + # Polling interval. Will only be polled if there are subscribers. + update_interval=SCAN_INTERVAL if should_poll else None, + ) + await coordinator.async_refresh() + + return coordinator + + +class AsusWrtDevInfo: + """Representation of a AsusWrt device info.""" + + def __init__(self, mac, name=None): + """Initialize a AsusWrt device info.""" + self._mac = mac + self._name = name + self._ip_address = None + self._last_activity = None + self._connected = False + + def update(self, dev_info=None, consider_home=0): + """Update AsusWrt device info.""" + utc_point_in_time = dt_util.utcnow() + if dev_info: + if not self._name: + self._name = dev_info.name or self._mac.replace(":", "_") + self._ip_address = dev_info.ip + self._last_activity = utc_point_in_time + self._connected = True + + elif self._connected: + self._connected = ( + utc_point_in_time - self._last_activity + ).total_seconds() < consider_home + self._ip_address = None + + @property + def is_connected(self): + """Return connected status.""" + return self._connected + + @property + def mac(self): + """Return device mac address.""" + return self._mac + + @property + def name(self): + """Return device name.""" + return self._name + + @property + def ip_address(self): + """Return device ip address.""" + return self._ip_address + + @property + def last_activity(self): + """Return device last activity.""" + return self._last_activity + + +class AsusWrtRouter: + """Representation of a AsusWrt router.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize a AsusWrt router.""" + self.hass = hass + self._entry = entry + + self._api: AsusWrt = None + self._protocol = entry.data[CONF_PROTOCOL] + self._host = entry.data[CONF_HOST] + + self._devices: dict[str, Any] = {} + self._connected_devices = 0 + self._connect_error = False + + self._sensors_data_handler: AsusWrtSensorDataHandler = None + self._sensors_coordinator: dict[str, Any] = {} + + self._on_close = [] + + self._options = { + CONF_DNSMASQ: DEFAULT_DNSMASQ, + CONF_INTERFACE: DEFAULT_INTERFACE, + CONF_REQUIRE_IP: True, + } + self._options.update(entry.options) + + async def setup(self) -> None: + """Set up a AsusWrt router.""" + self._api = get_api(self._entry.data, self._options) + + try: + await self._api.connection.async_connect() + except OSError as exp: + raise ConfigEntryNotReady from exp + + if not self._api.is_connected: + raise ConfigEntryNotReady + + # Load tracked entities from registry + entity_registry = await self.hass.helpers.entity_registry.async_get_registry() + track_entries = ( + self.hass.helpers.entity_registry.async_entries_for_config_entry( + entity_registry, self._entry.entry_id + ) + ) + for entry in track_entries: + if entry.domain == TRACKER_DOMAIN: + self._devices[entry.unique_id] = AsusWrtDevInfo( + entry.unique_id, entry.original_name + ) + + # Update devices + await self.update_devices() + + # Init Sensors + await self.init_sensors_coordinator() + + self.async_on_close( + async_track_time_interval(self.hass, self.update_all, SCAN_INTERVAL) + ) + + async def update_all(self, now: datetime | None = None) -> None: + """Update all AsusWrt platforms.""" + await self.update_devices() + + async def update_devices(self) -> None: + """Update AsusWrt devices tracker.""" + new_device = False + _LOGGER.debug("Checking devices for ASUS router %s", self._host) + try: + wrt_devices = await self._api.async_get_connected_devices() + except OSError as exc: + if not self._connect_error: + self._connect_error = True + _LOGGER.error( + "Error connecting to ASUS router %s for device update: %s", + self._host, + exc, + ) + return + + if self._connect_error: + self._connect_error = False + _LOGGER.info("Reconnected to ASUS router %s", self._host) + + consider_home = self._options.get( + CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds() + ) + track_unknown = self._options.get(CONF_TRACK_UNKNOWN, DEFAULT_TRACK_UNKNOWN) + + for device_mac in self._devices: + dev_info = wrt_devices.get(device_mac) + self._devices[device_mac].update(dev_info, consider_home) + + for device_mac, dev_info in wrt_devices.items(): + if device_mac in self._devices: + continue + if not track_unknown and not dev_info.name: + continue + new_device = True + device = AsusWrtDevInfo(device_mac) + device.update(dev_info) + self._devices[device_mac] = device + + async_dispatcher_send(self.hass, self.signal_device_update) + if new_device: + async_dispatcher_send(self.hass, self.signal_device_new) + + self._connected_devices = len(wrt_devices) + await self._update_unpolled_sensors() + + async def init_sensors_coordinator(self) -> None: + """Init AsusWrt sensors coordinators.""" + if self._sensors_data_handler: + return + + self._sensors_data_handler = AsusWrtSensorDataHandler(self.hass, self._api) + self._sensors_data_handler.update_device_count(self._connected_devices) + + conn_dev_coordinator = await self._sensors_data_handler.get_coordinator( + SENSORS_TYPE_COUNT, False + ) + self._sensors_coordinator[SENSORS_TYPE_COUNT] = { + KEY_COORDINATOR: conn_dev_coordinator, + KEY_SENSORS: [SENSOR_CONNECTED_DEVICE], + } + + bytes_coordinator = await self._sensors_data_handler.get_coordinator( + SENSORS_TYPE_BYTES + ) + self._sensors_coordinator[SENSORS_TYPE_BYTES] = { + KEY_COORDINATOR: bytes_coordinator, + KEY_SENSORS: [SENSOR_RX_BYTES, SENSOR_TX_BYTES], + } + + rates_coordinator = await self._sensors_data_handler.get_coordinator( + SENSORS_TYPE_RATES + ) + self._sensors_coordinator[SENSORS_TYPE_RATES] = { + KEY_COORDINATOR: rates_coordinator, + KEY_SENSORS: [SENSOR_RX_RATES, SENSOR_TX_RATES], + } + + async def _update_unpolled_sensors(self) -> None: + """Request refresh for AsusWrt unpolled sensors.""" + if not self._sensors_data_handler: + return + + if SENSORS_TYPE_COUNT in self._sensors_coordinator: + coordinator = self._sensors_coordinator[SENSORS_TYPE_COUNT][KEY_COORDINATOR] + if self._sensors_data_handler.update_device_count(self._connected_devices): + await coordinator.async_refresh() + + async def close(self) -> None: + """Close the connection.""" + if self._api is not None and self._protocol == PROTOCOL_TELNET: + self._api.connection.disconnect() + self._api = None + + for func in self._on_close: + func() + self._on_close.clear() + + @callback + def async_on_close(self, func: CALLBACK_TYPE) -> None: + """Add a function to call when router is closed.""" + self._on_close.append(func) + + def update_options(self, new_options: dict) -> bool: + """Update router options.""" + req_reload = False + for name, new_opt in new_options.items(): + if name in (CONF_REQ_RELOAD): + old_opt = self._options.get(name) + if not old_opt or old_opt != new_opt: + req_reload = True + break + + self._options.update(new_options) + return req_reload + + @property + def device_info(self) -> DeviceInfo: + """Return the device information.""" + return { + "identifiers": {(DOMAIN, "AsusWRT")}, + "name": self._host, + "model": "Asus Router", + "manufacturer": "Asus", + } + + @property + def signal_device_new(self) -> str: + """Event specific per AsusWrt entry to signal new device.""" + return f"{DOMAIN}-device-new" + + @property + def signal_device_update(self) -> str: + """Event specific per AsusWrt entry to signal updates in devices.""" + return f"{DOMAIN}-device-update" + + @property + def host(self) -> str: + """Return router hostname.""" + return self._host + + @property + def devices(self) -> dict[str, Any]: + """Return devices.""" + return self._devices + + @property + def sensors_coordinator(self) -> dict[str, Any]: + """Return sensors coordinators.""" + return self._sensors_coordinator + + @property + def api(self) -> AsusWrt: + """Return router API.""" + return self._api + + +def get_api(conf: dict, options: dict | None = None) -> AsusWrt: + """Get the AsusWrt API.""" + opt = options or {} + + return AsusWrt( + conf[CONF_HOST], + conf[CONF_PORT], + conf[CONF_PROTOCOL] == PROTOCOL_TELNET, + conf[CONF_USERNAME], + conf.get(CONF_PASSWORD, ""), + conf.get(CONF_SSH_KEY, ""), + conf[CONF_MODE], + opt.get(CONF_REQUIRE_IP, True), + interface=opt.get(CONF_INTERFACE, DEFAULT_INTERFACE), + dnsmasq=opt.get(CONF_DNSMASQ, DEFAULT_DNSMASQ), + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/sensor.py new file mode 100644 index 00000000000..6ec077620f6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/sensor.py @@ -0,0 +1,173 @@ +"""Asuswrt status sensors.""" +from __future__ import annotations + +import logging +from numbers import Number +from typing import Any + +from homeassistant.components.sensor import SensorEntity +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 .const import ( + DATA_ASUSWRT, + DOMAIN, + SENSOR_CONNECTED_DEVICE, + SENSOR_RX_BYTES, + SENSOR_RX_RATES, + SENSOR_TX_BYTES, + SENSOR_TX_RATES, +) +from .router import KEY_COORDINATOR, KEY_SENSORS, AsusWrtRouter + +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 = { + SENSOR_CONNECTED_DEVICE: { + SENSOR_NAME: "Devices Connected", + SENSOR_UNIT: UNIT_DEVICES, + SENSOR_FACTOR: 0, + SENSOR_ICON: "mdi:router-network", + SENSOR_DEVICE_CLASS: None, + SENSOR_DEFAULT_ENABLED: True, + }, + SENSOR_RX_RATES: { + SENSOR_NAME: "Download Speed", + SENSOR_UNIT: DATA_RATE_MEGABITS_PER_SECOND, + SENSOR_FACTOR: 125000, + SENSOR_ICON: "mdi:download-network", + SENSOR_DEVICE_CLASS: None, + }, + SENSOR_TX_RATES: { + SENSOR_NAME: "Upload Speed", + SENSOR_UNIT: DATA_RATE_MEGABITS_PER_SECOND, + SENSOR_FACTOR: 125000, + SENSOR_ICON: "mdi:upload-network", + SENSOR_DEVICE_CLASS: None, + }, + SENSOR_RX_BYTES: { + SENSOR_NAME: "Download", + SENSOR_UNIT: DATA_GIGABYTES, + SENSOR_FACTOR: 1000000000, + SENSOR_ICON: "mdi:download", + SENSOR_DEVICE_CLASS: None, + }, + SENSOR_TX_BYTES: { + SENSOR_NAME: "Upload", + SENSOR_UNIT: DATA_GIGABYTES, + SENSOR_FACTOR: 1000000000, + SENSOR_ICON: "mdi:upload", + SENSOR_DEVICE_CLASS: None, + }, +} + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +) -> None: + """Set up the sensors.""" + router: AsusWrtRouter = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT] + entities = [] + + 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] + ) + ) + + async_add_entities(entities, True) + + +class AsusWrtSensor(CoordinatorEntity, SensorEntity): + """Representation of a AsusWrt sensor.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + router: AsusWrtRouter, + sensor_type: str, + sensor: dict[str, Any], + ) -> None: + """Initialize a AsusWrt sensor.""" + super().__init__(coordinator) + self._router = router + self._sensor_type = sensor_type + self._name = f"{DEFAULT_PREFIX} {sensor[SENSOR_NAME]}" + self._unique_id = f"{DOMAIN} {self._name}" + self._unit = sensor[SENSOR_UNIT] + self._factor = sensor[SENSOR_FACTOR] + self._icon = sensor[SENSOR_ICON] + self._device_class = sensor[SENSOR_DEVICE_CLASS] + self._default_enabled = sensor.get(SENSOR_DEFAULT_ENABLED, False) + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self._default_enabled + + @property + def state(self) -> str: + """Return current state.""" + state = self.coordinator.data.get(self._sensor_type) + if state is None: + return None + if self._factor and isinstance(state, Number): + return round(state / self._factor, 2) + return state + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._unique_id + + @property + def name(self) -> str: + """Return the name.""" + return self._name + + @property + def unit_of_measurement(self) -> str: + """Return the unit.""" + return self._unit + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def device_class(self) -> str: + """Return the device_class.""" + return self._device_class + + @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 diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/strings.json new file mode 100644 index 00000000000..079ee35bf95 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/strings.json @@ -0,0 +1,45 @@ +{ + "config": { + "step": { + "user": { + "title": "AsusWRT", + "description": "Set required parameter to connect to your router", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "name": "[%key:common::config_flow::data::name%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "ssh_key": "Path to your SSH key file (instead of password)", + "protocol": "Communication protocol to use", + "port": "[%key:common::config_flow::data::port%]", + "mode": "[%key:common::config_flow::data::mode%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_host": "[%key:common::config_flow::error::invalid_host%]", + "pwd_and_ssh": "Only provide password or SSH key file", + "pwd_or_ssh": "Please provide password or SSH key file", + "ssh_not_file": "SSH key file not found", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + }, + "options": { + "step": { + "init": { + "title": "AsusWRT Options", + "data": { + "consider_home": "Seconds to wait before considering a device away", + "track_unknown": "Track unknown / unamed devices", + "interface": "The interface that you want statistics from (e.g. eth0,eth1 etc)", + "dnsmasq": "The location in the router of the dnsmasq.leases files", + "require_ip": "Devices must have IP (for access point mode)" + } + } + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/bg.json b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/bg.json new file mode 100644 index 00000000000..dbb5f415f92 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/bg.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "name": "\u0418\u043c\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/ca.json new file mode 100644 index 00000000000..446b08ecdfe --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/ca.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids", + "pwd_and_ssh": "Proporciona, nom\u00e9s, la contrasenya o el fitxer de claus SSH", + "pwd_or_ssh": "Proporciona la contrasenya o el fitxer de claus SSH", + "ssh_not_file": "No s'ha trobat el fitxer de claus SSH", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "mode": "Mode", + "name": "Nom", + "password": "Contrasenya", + "port": "Port", + "protocol": "Protocol de comunicacions a utilitzar", + "ssh_key": "Ruta al fitxer de claus SSH (en lloc de la contrasenya)", + "username": "Nom d'usuari" + }, + "description": "Introdueix el par\u00e0metre necessari per connectar-te al router", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Segons d'espera abans de considerar un dispositiu a fora", + "dnsmasq": "La ubicaci\u00f3 dins el router dels fitxers dnsmasq.leases", + "interface": "La interf\u00edcie de la qual obtenir les estad\u00edstiques (per exemple, eth0, eth1, etc.)", + "require_ip": "Els dispositius han de tenir una IP (per al mode de punt d'acc\u00e9s)", + "track_unknown": "Segueix dispositius desconeguts/sense nom" + }, + "title": "Opcions d'AsusWRT" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/cs.json new file mode 100644 index 00000000000..d9766e9a6d0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/cs.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_host": "Neplatn\u00fd hostitel nebo IP adresa", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "mode": "Re\u017eim", + "name": "Jm\u00e9no", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + }, + "title": "AsusWRT" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/de.json new file mode 100644 index 00000000000..36699d95753 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/de.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse", + "pwd_and_ssh": "Nur Passwort oder SSH-Schl\u00fcsseldatei angeben", + "pwd_or_ssh": "Bitte Passwort oder SSH-Schl\u00fcsseldatei angeben", + "ssh_not_file": "SSH-Schl\u00fcsseldatei nicht gefunden", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "mode": "Modus", + "name": "Name", + "password": "Passwort", + "port": "Port", + "protocol": "Zu verwendendes Kommunikationsprotokoll", + "ssh_key": "Pfad zu deiner SSH-Schl\u00fcsseldatei (anstelle des Passworts)", + "username": "Benutzername" + }, + "title": "" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "interface": "Schnittstelle, von der du Statistiken haben m\u00f6chtest (z.B. eth0, eth1 usw.)", + "require_ip": "Ger\u00e4te m\u00fcssen IP haben (f\u00fcr Zugangspunkt-Modus)" + }, + "title": "AsusWRT Optionen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/en.json new file mode 100644 index 00000000000..5ac87e277f4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/en.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_host": "Invalid hostname or IP address", + "pwd_and_ssh": "Only provide password or SSH key file", + "pwd_or_ssh": "Please provide password or SSH key file", + "ssh_not_file": "SSH key file not found", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "mode": "Mode", + "name": "Name", + "password": "Password", + "port": "Port", + "protocol": "Communication protocol to use", + "ssh_key": "Path to your SSH key file (instead of password)", + "username": "Username" + }, + "description": "Set required parameter to connect to your router", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Seconds to wait before considering a device away", + "dnsmasq": "The location in the router of the dnsmasq.leases files", + "interface": "The interface that you want statistics from (e.g. eth0,eth1 etc)", + "require_ip": "Devices must have IP (for access point mode)", + "track_unknown": "Track unknown / unamed devices" + }, + "title": "AsusWRT Options" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/es.json new file mode 100644 index 00000000000..c5792babf00 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/es.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos", + "pwd_and_ssh": "S\u00f3lo proporcionar la contrase\u00f1a o el archivo de clave SSH", + "pwd_or_ssh": "Por favor, proporcione la contrase\u00f1a o el archivo de clave SSH", + "ssh_not_file": "Archivo de clave SSH no encontrado", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host", + "mode": "Modo", + "name": "Nombre", + "password": "Contrase\u00f1a", + "port": "Puerto", + "protocol": "Protocolo de comunicaci\u00f3n a utilizar", + "ssh_key": "Ruta de acceso a su archivo de clave SSH (en lugar de la contrase\u00f1a)", + "username": "Nombre de usuario" + }, + "description": "Establezca los par\u00e1metros necesarios para conectarse a su router", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Segundos de espera antes de considerar un dispositivo ausente", + "dnsmasq": "La ubicaci\u00f3n en el router de los archivos dnsmasq.leases", + "interface": "La interfaz de la que desea obtener estad\u00edsticas (por ejemplo, eth0, eth1, etc.)", + "require_ip": "Los dispositivos deben tener IP (para el modo de punto de acceso)", + "track_unknown": "Seguimiento de dispositivos desconocidos / sin nombre" + }, + "title": "Opciones de AsusWRT" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/et.json new file mode 100644 index 00000000000..8cc14b7353b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/et.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_host": "Sobimatu hostinimi v\u00f5i IP-aadress", + "pwd_and_ssh": "Sisesta ainult parooli v\u00f5i SSH v\u00f5tmefail", + "pwd_or_ssh": "Sisesta parool v\u00f5i SSH v\u00f5tmefail", + "ssh_not_file": "SSH v\u00f5tmefaili ei leitud", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host", + "mode": "Re\u017eiim", + "name": "Nimi", + "password": "Salas\u00f5na", + "port": "Port", + "protocol": "Kasutatav sideprotokoll", + "ssh_key": "Rada SSH v\u00f5tmefailini (parooli asemel)", + "username": "Kasutajanimi" + }, + "description": "M\u00e4\u00e4ra ruuteriga \u00fchenduse loomiseks vajalik parameeter", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Mitu sekundit oodata, enne kui lugeda seade eemal olevaks", + "dnsmasq": "Dnsmasq.leases failide asukoht ruuteris", + "interface": "Liides kust soovite statistikat (n\u00e4iteks eth0, eth1 jne.)", + "require_ip": "Seadmetel peab olema IP (p\u00e4\u00e4supunkti re\u017eiimi jaoks)", + "track_unknown": "J\u00e4lgi tundmatuid / nimetamata seadmeid" + }, + "title": "AsusWRT valikud" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/fr.json new file mode 100644 index 00000000000..0d53f3f24cf --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/fr.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", + "pwd_and_ssh": "Fournissez uniquement le mot de passe ou le fichier de cl\u00e9 SSH", + "pwd_or_ssh": "Veuillez fournir un mot de passe ou un fichier de cl\u00e9 SSH", + "ssh_not_file": "Fichier cl\u00e9 SSH non trouv\u00e9", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "mode": "Mode", + "name": "Nom", + "password": "Mot de passe", + "port": "Port", + "protocol": "Protocole de communication \u00e0 utiliser", + "ssh_key": "Chemin d'acc\u00e8s \u00e0 votre fichier de cl\u00e9s SSH (au lieu du mot de passe)", + "username": "Nom d'utilisateur" + }, + "description": "D\u00e9finissez les param\u00e8tres n\u00e9cessaires pour vous connecter \u00e0 votre routeur", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Quelques secondes d'attente avant d'envisager l'abandon d'un appareil", + "dnsmasq": "L\u2019emplacement dans le routeur des fichiers dnsmasq.leases", + "interface": "L'interface \u00e0 partir de laquelle vous souhaitez obtenir des statistiques (e.g. eth0,eth1 etc)", + "require_ip": "Les appareils doivent avoir une IP (pour le mode point d'acc\u00e8s)", + "track_unknown": "Traquer les appareils inconnus / non identifi\u00e9s" + }, + "title": "Options AsusWRT" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/hu.json new file mode 100644 index 00000000000..4f47781a15c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/hu.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm", + "ssh_not_file": "Az SSH kulcsf\u00e1jl nem tal\u00e1lhat\u00f3", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "Hoszt", + "mode": "M\u00f3d", + "name": "N\u00e9v", + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "title": "AsusWRT Be\u00e1ll\u00edt\u00e1sok" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/id.json new file mode 100644 index 00000000000..aa4eebd1f86 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/id.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_host": "Nama host atau alamat IP tidak valid", + "pwd_and_ssh": "Hanya berikan kata sandi atau file kunci SSH", + "pwd_or_ssh": "Harap berikan kata sandi atau file kunci SSH", + "ssh_not_file": "File kunci SSH tidak ditemukan", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host", + "mode": "Mode", + "name": "Nama", + "password": "Kata Sandi", + "port": "Port", + "protocol": "Protokol komunikasi yang akan digunakan", + "ssh_key": "Jalur ke file kunci SSH Anda (bukan kata sandi)", + "username": "Nama Pengguna" + }, + "description": "Tetapkan parameter yang diperlukan untuk terhubung ke router Anda", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Tenggang waktu dalam detik untuk perangkat dianggap sebagai keluar", + "dnsmasq": "Lokasi dnsmasq.leases di router file", + "interface": "Antarmuka statistik yang diinginkan (mis. eth0, eth1, dll)", + "require_ip": "Perangkat harus memiliki IP (untuk mode titik akses)", + "track_unknown": "Lacak perangkat yang tidak dikenal/tidak bernama" + }, + "title": "Opsi AsusWRT" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/it.json new file mode 100644 index 00000000000..d266cabbed4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/it.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_host": "Nome host o indirizzo IP non valido", + "pwd_and_ssh": "Fornire solo la password o il file della chiave SSH", + "pwd_or_ssh": "Si prega di fornire la password o il file della chiave SSH", + "ssh_not_file": "File chiave SSH non trovato", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "mode": "Modalit\u00e0", + "name": "Nome", + "password": "Password", + "port": "Porta", + "protocol": "Protocollo di comunicazione da utilizzare", + "ssh_key": "Percorso del file della chiave SSH (invece della password)", + "username": "Nome utente" + }, + "description": "Imposta il parametro richiesto per collegarti al tuo router", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Secondi di attesa prima di considerare un dispositivo lontano", + "dnsmasq": "La posizione nel router dei file dnsmasq.leases", + "interface": "L'interfaccia da cui si desidera ottenere statistiche (ad esempio eth0, eth1, ecc.)", + "require_ip": "I dispositivi devono avere un IP (per la modalit\u00e0 punto di accesso)", + "track_unknown": "Tieni traccia dei dispositivi sconosciuti / non denominati" + }, + "title": "Opzioni AsusWRT" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/ko.json new file mode 100644 index 00000000000..4e60a0d15b0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/ko.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "pwd_and_ssh": "\ube44\ubc00\ubc88\ud638 \ub610\ub294 SSH \ud0a4 \ud30c\uc77c\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4", + "pwd_or_ssh": "\ube44\ubc00\ubc88\ud638 \ub610\ub294 SSH \ud0a4 \ud30c\uc77c\uc744 \ub123\uc5b4\uc8fc\uc138\uc694", + "ssh_not_file": "SSH \ud0a4 \ud30c\uc77c\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "mode": "\ubaa8\ub4dc", + "name": "\uc774\ub984", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "protocol": "\uc0ac\uc6a9\ud560 \ud1b5\uc2e0 \ud504\ub85c\ud1a0\ucf5c", + "ssh_key": "SSH \ud0a4 \ud30c\uc77c\uc758 \uacbd\ub85c (\ube44\ubc00\ubc88\ud638 \ub300\uccb4)", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "\ub77c\uc6b0\ud130\uc5d0 \uc5f0\uacb0\ud558\ub294 \ub370 \ud544\uc694\ud55c \ub9e4\uac1c \ubcc0\uc218\ub97c \uc124\uc815\ud558\uae30", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "\uae30\uae30\uac00 \uc678\ucd9c \uc0c1\ud0dc\ub85c \uac04\uc8fc\ub418\uae30 \uc804\uc5d0 \uae30\ub2e4\ub9ac\ub294 \uc2dc\uac04 (\ucd08)", + "dnsmasq": "dnsmasq.lease \ud30c\uc77c\uc758 \ub77c\uc6b0\ud130 \uc704\uce58", + "interface": "\ud1b5\uacc4\ub97c \uc6d0\ud558\ub294 \uc778\ud130\ud398\uc774\uc2a4 (\uc608: eth0, eth1 \ub4f1)", + "require_ip": "\uae30\uae30\uc5d0\ub294 IP\uac00 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4 (\uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8 \ubaa8\ub4dc\uc778 \uacbd\uc6b0)", + "track_unknown": "\uc54c \uc218 \uc5c6\uac70\ub098 \uc774\ub984\uc774 \uc5c6\ub294 \uae30\uae30 \ucd94\uc801\ud558\uae30" + }, + "title": "AsusWRT \uc635\uc158" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/lb.json new file mode 100644 index 00000000000..0c1512ac67a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/lb.json @@ -0,0 +1,12 @@ +{ + "config": { + "error": { + "ssh_not_file": "SSH Schl\u00ebssel Datei net fonnt" + }, + "step": { + "user": { + "title": "AsusWRT" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/nl.json new file mode 100644 index 00000000000..f6f347f771f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/nl.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_host": "Ongeldige hostnaam of IP-adres", + "pwd_and_ssh": "Geef alleen wachtwoord of SSH-sleutelbestand op", + "pwd_or_ssh": "Geef een wachtwoord of SSH-sleutelbestand op", + "ssh_not_file": "SSH-sleutelbestand niet gevonden", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host", + "mode": "Mode", + "name": "Naam", + "password": "Wachtwoord", + "port": "Poort", + "protocol": "Te gebruiken communicatieprotocol", + "ssh_key": "Pad naar uw SSH-sleutelbestand (in plaats van wachtwoord)", + "username": "Gebruikersnaam" + }, + "description": "Stel de vereiste parameter in om verbinding te maken met uw router", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Aantal seconden dat wordt gewacht voordat een apparaat als afwezig wordt beschouwd", + "dnsmasq": "De locatie in de router van de dnsmasq.leases-bestanden", + "interface": "De interface waarvan u statistieken wilt (bijv. Eth0, eth1 enz.)", + "require_ip": "Apparaten moeten een IP-adres hebben (voor toegangspuntmodus)", + "track_unknown": "Volg onbekende / naamloze apparaten" + }, + "title": "AsusWRT-opties" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/no.json new file mode 100644 index 00000000000..42c9798d495 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/no.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_host": "Ugyldig vertsnavn eller IP-adresse", + "pwd_and_ssh": "Oppgi bare passord eller SSH-n\u00f8kkelfil", + "pwd_or_ssh": "Oppgi passord eller SSH-n\u00f8kkelfil", + "ssh_not_file": "Finner ikke SSH-n\u00f8kkelfilen", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "mode": "Modus", + "name": "Navn", + "password": "Passord", + "port": "Port", + "protocol": "Kommunikasjonsprotokoll som skal brukes", + "ssh_key": "Bane til SSH-n\u00f8kkelfilen (i stedet for passord)", + "username": "Brukernavn" + }, + "description": "Sett \u00f8nsket parameter for \u00e5 koble til ruteren", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Sekunder \u00e5 vente f\u00f8r du vurderer en enhet borte", + "dnsmasq": "Plasseringen i ruteren til dnsmasq.leases-filene", + "interface": "Grensesnittet du vil ha statistikk fra (f.eks. Eth0, eth1 osv.)", + "require_ip": "Enheter m\u00e5 ha IP (for tilgangspunktmodus)", + "track_unknown": "Spor ukjente / ikke-navngitte enheter" + }, + "title": "AsusWRT-alternativer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/pl.json new file mode 100644 index 00000000000..9fd5d00b1c4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/pl.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_host": "Nieprawid\u0142owa nazwa hosta lub adres IP", + "pwd_and_ssh": "Podaj tylko has\u0142o lub plik z kluczem SSH", + "pwd_or_ssh": "Podaj has\u0142o lub plik z kluczem SSH", + "ssh_not_file": "Nie znaleziono pliku z kluczem SSH", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "mode": "Tryb", + "name": "Nazwa", + "password": "Has\u0142o", + "port": "Port", + "protocol": "Wybierz protok\u00f3\u0142 komunikacyjny", + "ssh_key": "\u015acie\u017cka do pliku z kluczem SSH (zamiast has\u0142a)", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Ustaw wymagany parametr, aby po\u0142\u0105czy\u0107 si\u0119 z routerem", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Czas w sekundach, zanim urz\u0105dzenie utrzyma stan \"poza domem\"", + "dnsmasq": "Lokalizacja w routerze plik\u00f3w dnsmasq.leases", + "interface": "Interfejs, z kt\u00f3rego chcesz uzyska\u0107 statystyki (np. eth0, eth1 itp.)", + "require_ip": "Urz\u0105dzenia musz\u0105 mie\u0107 adres IP (w trybie punktu dost\u0119pu)", + "track_unknown": "\u015aled\u017a nieznane / nienazwane urz\u0105dzenia" + }, + "title": "Opcje AsusWRT" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/ru.json new file mode 100644 index 00000000000..a2090b1faf6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/ru.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "pwd_and_ssh": "\u041d\u0443\u0436\u043d\u043e \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u0430\u0440\u043e\u043b\u044c \u0438\u043b\u0438 \u0442\u043e\u043b\u044c\u043a\u043e \u0444\u0430\u0439\u043b \u043a\u043b\u044e\u0447\u0430 SSH.", + "pwd_or_ssh": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0438\u043b\u0438 \u0444\u0430\u0439\u043b \u043a\u043b\u044e\u0447\u0430 SSH.", + "ssh_not_file": "\u0424\u0430\u0439\u043b \u043a\u043b\u044e\u0447\u0430 SSH \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "mode": "\u0420\u0435\u0436\u0438\u043c", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0441\u0432\u044f\u0437\u0438", + "ssh_key": "\u041f\u0443\u0442\u044c \u0444\u0430\u0439\u043b\u0443 \u043a\u043b\u044e\u0447\u0435\u0439 SSH (\u0432\u043c\u0435\u0441\u0442\u043e \u043f\u0430\u0440\u043e\u043b\u044f)", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0440\u043e\u0443\u0442\u0435\u0440\u0443.", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "\u0412\u0440\u0435\u043c\u044f \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0434\u043e\u043c\u0430\"", + "dnsmasq": "\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0432 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0435 \u0444\u0430\u0439\u043b\u043e\u0432 dnsmasq.leases", + "interface": "\u0418\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441, \u0441 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0443 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, eth0, eth1 \u0438 \u0442. \u0434.)", + "require_ip": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0441 IP-\u0430\u0434\u0440\u0435\u0441\u043e\u043c (\u0434\u043b\u044f \u0440\u0435\u0436\u0438\u043c\u0430 \u0442\u043e\u0447\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430)", + "track_unknown": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0435/\u0431\u0435\u0437\u044b\u043c\u044f\u043d\u043d\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AsusWRT" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/zh-Hant.json new file mode 100644 index 00000000000..8caddacd23e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/asuswrt/translations/zh-Hant.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740", + "pwd_and_ssh": "\u50c5\u63d0\u4f9b\u5bc6\u78bc\u6216 SSH \u5bc6\u9470\u6a94\u6848", + "pwd_or_ssh": "\u8acb\u8f38\u5165\u5bc6\u78bc\u6216 SSH \u5bc6\u9470\u6a94\u6848", + "ssh_not_file": "\u627e\u4e0d\u5230 SSH \u5bc6\u9470\u6a94\u6848", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "mode": "\u6a21\u5f0f", + "name": "\u540d\u7a31", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "protocol": "\u4f7f\u7528\u901a\u8a0a\u606f\u5354\u5b9a", + "ssh_key": "SSH \u5bc6\u9470\u6a94\u6848\u8def\u5f91\uff08\u975e\u5bc6\u78bc\uff09", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8a2d\u5b9a\u6240\u9700\u53c3\u6578\u4ee5\u9023\u7dda\u81f3\u8def\u7531\u5668", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "\u8996\u70ba\u96e2\u958b\u7684\u7b49\u5019\u79d2\u6578", + "dnsmasq": "dnsmasq.leases \u6a94\u6848\u65bc\u8def\u7531\u5668\u4e2d\u6240\u5728\u4f4d\u7f6e", + "interface": "\u6240\u8981\u9032\u884c\u7d71\u8a08\u7684\u4ecb\u9762\u53e3\uff08\u4f8b\u5982 eth0\u3001eth1 \u7b49\uff09", + "require_ip": "\u88dd\u7f6e\u5fc5\u9808\u5177\u6709 IP\uff08\u7528\u65bc AP \u6a21\u5f0f\uff09", + "track_unknown": "\u8ffd\u8e64\u672a\u77e5 / \u672a\u547d\u540d\u88dd\u7f6e" + }, + "title": "AsusWRT \u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/__init__.py new file mode 100644 index 00000000000..e6347563bc2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/__init__.py @@ -0,0 +1,101 @@ +"""The ATAG Integration.""" +from datetime import timedelta +import logging + +import async_timeout +from pyatag import AtagException, AtagOne + +from homeassistant.components.climate import DOMAIN as CLIMATE +from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.components.water_heater import DOMAIN as WATER_HEATER +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "atag" +PLATFORMS = [CLIMATE, WATER_HEATER, SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Atag integration from a config entry.""" + + async def _async_update_data(): + """Update data via library.""" + with async_timeout.timeout(20): + try: + await atag.update() + except AtagException as err: + raise UpdateFailed(err) from err + return atag + + atag = AtagOne( + session=async_get_clientsession(hass), **entry.data, device=entry.unique_id + ) + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=DOMAIN.title(), + update_method=_async_update_data, + update_interval=timedelta(seconds=60), + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + if entry.unique_id is None: + hass.config_entries.async_update_entry(entry, unique_id=atag.id) + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass, entry): + """Unload Atag 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 AtagEntity(CoordinatorEntity): + """Defines a base Atag entity.""" + + def __init__(self, coordinator: DataUpdateCoordinator, atag_id: str) -> None: + """Initialize the Atag entity.""" + super().__init__(coordinator) + + self._id = atag_id + self._name = DOMAIN.title() + + @property + def device_info(self) -> DeviceInfo: + """Return info for device registry.""" + device = self.coordinator.data.id + version = self.coordinator.data.apiversion + return { + "identifiers": {(DOMAIN, device)}, + "name": "Atag Thermostat", + "model": "Atag One", + "sw_version": version, + "manufacturer": "Atag", + } + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def unique_id(self): + """Return a unique ID to use for this entity.""" + return f"{self.coordinator.data.id}-{self._id}" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/climate.py b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/climate.py new file mode 100644 index 00000000000..da7e6a14a73 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/climate.py @@ -0,0 +1,102 @@ +"""Initialization of ATAG One climate platform.""" +from __future__ import annotations + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + PRESET_AWAY, + PRESET_BOOST, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ATTR_TEMPERATURE + +from . import CLIMATE, DOMAIN, AtagEntity + +PRESET_MAP = { + "Manual": "manual", + "Auto": "automatic", + "Extend": "extend", + PRESET_AWAY: "vacation", + PRESET_BOOST: "fireplace", +} +PRESET_INVERTED = {v: k for k, v in PRESET_MAP.items()} +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE +HVAC_MODES = [HVAC_MODE_AUTO, HVAC_MODE_HEAT] + + +async def async_setup_entry(hass, entry, async_add_entities): + """Load a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([AtagThermostat(coordinator, CLIMATE)]) + + +class AtagThermostat(AtagEntity, ClimateEntity): + """Atag climate device.""" + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + @property + def hvac_mode(self) -> str | None: + """Return hvac operation ie. heat, cool mode.""" + if self.coordinator.data.climate.hvac_mode in HVAC_MODES: + return self.coordinator.data.climate.hvac_mode + return None + + @property + def hvac_modes(self) -> list[str]: + """Return the list of available hvac operation modes.""" + return HVAC_MODES + + @property + def hvac_action(self) -> str | None: + """Return the current running hvac operation.""" + is_active = self.coordinator.data.climate.status + return CURRENT_HVAC_HEAT if is_active else CURRENT_HVAC_IDLE + + @property + def temperature_unit(self) -> str | None: + """Return the unit of measurement.""" + return self.coordinator.data.climate.temp_unit + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + return self.coordinator.data.climate.temperature + + @property + def target_temperature(self) -> float | None: + """Return the temperature we try to reach.""" + return self.coordinator.data.climate.target_temperature + + @property + def preset_mode(self) -> str | None: + """Return the current preset mode, e.g., auto, manual, fireplace, extend, etc.""" + preset = self.coordinator.data.climate.preset_mode + return PRESET_INVERTED.get(preset) + + @property + def preset_modes(self) -> list[str] | None: + """Return a list of available preset modes.""" + return list(PRESET_MAP.keys()) + + async def async_set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + await self.coordinator.data.climate.set_temp(kwargs.get(ATTR_TEMPERATURE)) + self.async_write_ha_state() + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + await self.coordinator.data.climate.set_hvac_mode(hvac_mode) + self.async_write_ha_state() + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + await self.coordinator.data.climate.set_preset_mode(PRESET_MAP[preset_mode]) + self.async_write_ha_state() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/config_flow.py new file mode 100644 index 00000000000..ecebec717f4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/config_flow.py @@ -0,0 +1,48 @@ +"""Config flow for the Atag component.""" +import pyatag +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from . import DOMAIN + +DATA_SCHEMA = { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=pyatag.const.DEFAULT_PORT): vol.Coerce(int), +} + + +class AtagConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for Atag.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + + if not user_input: + return await self._show_form() + + atag = pyatag.AtagOne(session=async_get_clientsession(self.hass), **user_input) + try: + await atag.update() + + except pyatag.Unauthorized: + return await self._show_form({"base": "unauthorized"}) + except pyatag.AtagException: + return await self._show_form({"base": "cannot_connect"}) + + await self.async_set_unique_id(atag.id) + self._abort_if_unique_id_configured(updates=user_input) + + return self.async_create_entry(title=atag.id, data=user_input) + + async def _show_form(self, errors=None): + """Show the form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema(DATA_SCHEMA), + errors=errors if errors else {}, + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/manifest.json new file mode 100644 index 00000000000..eb9dc54ecd2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "atag", + "name": "Atag", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/atag/", + "requirements": ["pyatag==0.3.5.3"], + "codeowners": ["@MatsNL"], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/sensor.py new file mode 100644 index 00000000000..88ccbdc899f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/sensor.py @@ -0,0 +1,72 @@ +"""Initialization of ATAG One sensor platform.""" +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import ( + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, + PRESSURE_BAR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TIME_HOURS, +) + +from . import DOMAIN, AtagEntity + +SENSORS = { + "Outside Temperature": "outside_temp", + "Average Outside Temperature": "tout_avg", + "Weather Status": "weather_status", + "CH Water Pressure": "ch_water_pres", + "CH Water Temperature": "ch_water_temp", + "CH Return Temperature": "ch_return_temp", + "Burning Hours": "burning_hours", + "Flame": "rel_mod_level", +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Initialize sensor platform from config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities([AtagSensor(coordinator, sensor) for sensor in SENSORS]) + + +class AtagSensor(AtagEntity, SensorEntity): + """Representation of a AtagOne Sensor.""" + + def __init__(self, coordinator, sensor): + """Initialize Atag sensor.""" + super().__init__(coordinator, SENSORS[sensor]) + self._name = sensor + + @property + def state(self): + """Return the state of the sensor.""" + return self.coordinator.data.report[self._id].state + + @property + def icon(self): + """Return icon.""" + return self.coordinator.data.report[self._id].icon + + @property + def device_class(self): + """Return deviceclass.""" + if self.coordinator.data.report[self._id].sensorclass in [ + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + ]: + return self.coordinator.data.report[self._id].sensorclass + return None + + @property + def unit_of_measurement(self): + """Return measure.""" + if self.coordinator.data.report[self._id].measure in [ + PRESSURE_BAR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + PERCENTAGE, + TIME_HOURS, + ]: + return self.coordinator.data.report[self._id].measure + return None diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/strings.json new file mode 100644 index 00000000000..39ed972524d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "title": "Connect to the device", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + } + } + }, + "error": { + "unauthorized": "Pairing denied, check device for auth request", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/ca.json new file mode 100644 index 00000000000..537d347a228 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unauthorized": "La vinculaci\u00f3 s'ha denegat, comprova si hi ha una sol\u00b7licitud d'autenticaci\u00f3 al dispositiu" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port" + }, + "title": "Connexi\u00f3 amb el dispositiu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/cs.json new file mode 100644 index 00000000000..a94ba7fe391 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unauthorized": "P\u00e1rov\u00e1n\u00ed bylo odm\u00edtnuto, zkontrolujte po\u017eadavek na autorizaci na za\u0159\u00edzen\u00ed" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + }, + "title": "P\u0159ipojen\u00ed k za\u0159\u00edzen\u00ed" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/de.json new file mode 100644 index 00000000000..72e8c69cc26 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Dieses Ger\u00e4t wurde bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unauthorized": "Pairing verweigert, Ger\u00e4t auf Authentifizierungsanforderung pr\u00fcfen" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Stellen Sie eine Verbindung zum Ger\u00e4t her" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/en.json new file mode 100644 index 00000000000..1cd25c2a9b2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "unauthorized": "Pairing denied, check device for auth request" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Connect to the device" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/es-419.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/es-419.json new file mode 100644 index 00000000000..92e7fae8703 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/es-419.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Solo se puede agregar un dispositivo Atag a Home Assistant" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Puerto (10000)" + }, + "title": "Conectarse al dispositivo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/es.json new file mode 100644 index 00000000000..ed89c8c385c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Este dispositivo ya ha sido a\u00f1adido a HomeAssistant" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "unauthorized": "Emparejamiento denegado, comprobar el dispositivo para la solicitud de autorizaci\u00f3n" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Puerto" + }, + "title": "Conectarse al dispositivo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/et.json new file mode 100644 index 00000000000..fd157a3d541 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unauthorized": "Sidumine on keelatud, kontrolli seadme tuvastamistaotlust" + }, + "step": { + "user": { + "data": { + "host": "", + "port": "" + }, + "title": "\u00dchendu seadmega" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/fi.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/fi.json new file mode 100644 index 00000000000..0bf4c236a9b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/fi.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Palvelin", + "port": "Portti (10000)" + }, + "title": "Yhdist\u00e4 laitteeseen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/fr.json new file mode 100644 index 00000000000..a0f4b9f3808 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Un seul appareil Atag peut \u00eatre ajout\u00e9 \u00e0 Home Assistant" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unauthorized": "Pairage refus\u00e9, v\u00e9rifiez la demande d'authentification de l'appareil" + }, + "step": { + "user": { + "data": { + "host": "Nom d'h\u00f4te ou adresse IP", + "port": "Port" + }, + "title": "Se connecter \u00e0 l'appareil" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/hi.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/hi.json new file mode 100644 index 00000000000..ad2657e96a2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/hi.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Host", + "port": "\u092a\u094b\u0930\u094d\u091f (10000)" + }, + "title": "\u0921\u093f\u0935\u093e\u0907\u0938 \u0938\u0947 \u0915\u0928\u0947\u0915\u094d\u091f \u0915\u0930\u0947\u0902" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/hu.json new file mode 100644 index 00000000000..134f3bedfe8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "Hoszt", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/id.json new file mode 100644 index 00000000000..33f4cf62b2e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unauthorized": "Pemasangan ditolak, periksa perangkat untuk permintaan autentikasi" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Hubungkan ke perangkat" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/it.json new file mode 100644 index 00000000000..1bc473a6001 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unauthorized": "Associazione negata, controllare il dispositivo per la richiesta di autenticazione" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta" + }, + "title": "Connettersi al dispositivo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/ka.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/ka.json new file mode 100644 index 00000000000..fa4c9c0abd3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/ka.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/ko.json new file mode 100644 index 00000000000..25de83f70c3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "unauthorized": "\ud398\uc5b4\ub9c1\uc774 \uac70\ubd80\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc778\uc99d \uc694\uccad \uae30\uae30\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8" + }, + "title": "\uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/lb.json new file mode 100644 index 00000000000..1238d2bcf52 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/lb.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "unauthorized": "Kopplung verweigert, iwwerpr\u00e9if den Apparat fir auth request" + }, + "step": { + "user": { + "data": { + "host": "Apparat", + "port": "Port" + }, + "title": "Mam Apparat verbannen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/nl.json new file mode 100644 index 00000000000..98200dd3f6e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Er kan slechts \u00e9\u00e9n Atag-apparaat worden toegevoegd aan Home Assistant " + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unauthorized": "Koppelen geweigerd, controleer apparaat op autorisatieverzoek" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Poort " + }, + "title": "Verbinding maken met het apparaat" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/no.json new file mode 100644 index 00000000000..6a2736ae8b4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unauthorized": "Parring nektet, sjekk enheten for autorisasjonsforesp\u00f8rsel" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "port": "Port" + }, + "title": "Koble til enheten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/pl.json new file mode 100644 index 00000000000..bdd38a3d980 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unauthorized": "Odmowa parowania, sprawd\u017a urz\u0105dzenie pod k\u0105tem \u017c\u0105dania autoryzacji" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port" + }, + "title": "Po\u0142\u0105czenie z urz\u0105dzeniem" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/pt-BR.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/pt-BR.json new file mode 100644 index 00000000000..5d9d5079110 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/pt-BR.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Este dispositivo j\u00e1 foi adicionado ao Home Assistant" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/pt.json new file mode 100644 index 00000000000..fa5aa3de317 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/ru.json new file mode 100644 index 00000000000..feb21d3addd --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/ru.json @@ -0,0 +1,20 @@ +{ + "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.", + "unauthorized": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0437\u0430\u043f\u0440\u0435\u0449\u0435\u043d\u043e, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0430 \u0437\u0430\u043f\u0440\u043e\u0441 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/sl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/sl.json new file mode 100644 index 00000000000..8c939f153d6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/sl.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Home Assistant-u lahko dodate samo eno napravo Atag" + }, + "step": { + "user": { + "data": { + "host": "Gostitelj", + "port": "Vrata (10000)" + }, + "title": "Pove\u017eite se z napravo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/sv.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/sv.json new file mode 100644 index 00000000000..ae07cfa6221 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port (10000)" + }, + "title": "Anslut till enheten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/tr.json new file mode 100644 index 00000000000..577ed02cdca --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unauthorized": "E\u015fle\u015ftirme reddedildi, kimlik do\u011frulama iste\u011fi i\u00e7in cihaz\u0131 kontrol edin" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + }, + "title": "Cihaza ba\u011flan\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/uk.json new file mode 100644 index 00000000000..d6b259debc6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unauthorized": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u0437\u0430\u0431\u043e\u0440\u043e\u043d\u0435\u043d\u043e, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0430 \u0437\u0430\u043f\u0438\u0442 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/zh-Hans.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/zh-Hans.json new file mode 100644 index 00000000000..2941dfd9383 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/zh-Hant.json new file mode 100644 index 00000000000..c9904a954d7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unauthorized": "\u914d\u5c0d\u906d\u62d2\uff0c\u8acb\u6aa2\u67e5\u88dd\u7f6e\u8a8d\u8b49\u8acb\u6c42" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + }, + "title": "\u9023\u7dda\u81f3\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atag/water_heater.py b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/water_heater.py new file mode 100644 index 00000000000..dac56edf89d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atag/water_heater.py @@ -0,0 +1,69 @@ +"""ATAG water heater component.""" +from homeassistant.components.water_heater import ( + ATTR_TEMPERATURE, + STATE_ECO, + STATE_PERFORMANCE, + WaterHeaterEntity, +) +from homeassistant.const import STATE_OFF, TEMP_CELSIUS + +from . import DOMAIN, WATER_HEATER, AtagEntity + +SUPPORT_FLAGS_HEATER = 0 +OPERATION_LIST = [STATE_OFF, STATE_ECO, STATE_PERFORMANCE] + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Initialize DHW device from config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities([AtagWaterHeater(coordinator, WATER_HEATER)]) + + +class AtagWaterHeater(AtagEntity, WaterHeaterEntity): + """Representation of an ATAG water heater.""" + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS_HEATER + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def current_temperature(self): + """Return the current temperature.""" + return self.coordinator.data.dhw.temperature + + @property + def current_operation(self): + """Return current operation.""" + operation = self.coordinator.data.dhw.current_operation + return operation if operation in self.operation_list else STATE_OFF + + @property + def operation_list(self): + """List of available operation modes.""" + return OPERATION_LIST + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + if await self.coordinator.data.dhw.set_temp(kwargs.get(ATTR_TEMPERATURE)): + self.async_write_ha_state() + + @property + def target_temperature(self): + """Return the setpoint if water demand, otherwise return base temp (comfort level).""" + return self.coordinator.data.dhw.target_temperature + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self.coordinator.data.dhw.max_temp + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self.coordinator.data.dhw.min_temp diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aten_pe/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aten_pe/__init__.py new file mode 100644 index 00000000000..2a0fb277a48 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aten_pe/__init__.py @@ -0,0 +1 @@ +"""The ATEN PE component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aten_pe/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aten_pe/manifest.json new file mode 100644 index 00000000000..b5a35345086 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aten_pe/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "aten_pe", + "name": "ATEN Rack PDU", + "documentation": "https://www.home-assistant.io/integrations/aten_pe", + "requirements": ["atenpdu==0.3.0"], + "codeowners": ["@mtdcr"], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aten_pe/switch.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aten_pe/switch.py new file mode 100644 index 00000000000..1bf54085064 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aten_pe/switch.py @@ -0,0 +1,122 @@ +"""The ATEN PE switch component.""" + +import logging + +from atenpdu import AtenPE, AtenPEError +import voluptuous as vol + +from homeassistant.components.switch import ( + DEVICE_CLASS_OUTLET, + PLATFORM_SCHEMA, + SwitchEntity, +) +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_USERNAME +from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_AUTH_KEY = "auth_key" +CONF_COMMUNITY = "community" +CONF_PRIV_KEY = "priv_key" +DEFAULT_COMMUNITY = "private" +DEFAULT_PORT = "161" +DEFAULT_USERNAME = "administrator" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_COMMUNITY, default=DEFAULT_COMMUNITY): cv.string, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_AUTH_KEY): cv.string, + vol.Optional(CONF_PRIV_KEY): cv.string, + } +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the ATEN PE switch.""" + node = config[CONF_HOST] + serv = config[CONF_PORT] + + dev = AtenPE( + node=node, + serv=serv, + community=config[CONF_COMMUNITY], + username=config[CONF_USERNAME], + authkey=config.get(CONF_AUTH_KEY), + privkey=config.get(CONF_PRIV_KEY), + ) + + try: + await hass.async_add_executor_job(dev.initialize) + mac = await dev.deviceMAC() + outlets = dev.outlets() + except AtenPEError as exc: + _LOGGER.error("Failed to initialize %s:%s: %s", node, serv, str(exc)) + raise PlatformNotReady from exc + + switches = [] + async for outlet in outlets: + switches.append(AtenSwitch(dev, mac, outlet.id, outlet.name)) + + async_add_entities(switches) + + +class AtenSwitch(SwitchEntity): + """Represents an ATEN PE switch.""" + + def __init__(self, device, mac, outlet, name): + """Initialize an ATEN PE switch.""" + self._device = device + self._mac = mac + self._outlet = outlet + self._name = name or f"Outlet {outlet}" + self._enabled = False + self._outlet_power = 0.0 + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return f"{self._mac}-{self._outlet}" + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def device_class(self) -> str: + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_OUTLET + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self._enabled + + @property + def current_power_w(self) -> float: + """Return the current power usage in W.""" + return self._outlet_power + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + await self._device.setOutletStatus(self._outlet, "on") + self._enabled = True + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + await self._device.setOutletStatus(self._outlet, "off") + self._enabled = False + + async def async_update(self): + """Process update from entity.""" + status = await self._device.displayOutletStatus(self._outlet) + if status == "on": + self._enabled = True + self._outlet_power = await self._device.outletPower(self._outlet) + elif status == "off": + self._enabled = False + self._outlet_power = 0.0 diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atome/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/atome/__init__.py new file mode 100644 index 00000000000..6f524606a81 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atome/__init__.py @@ -0,0 +1 @@ +"""Support for Atome devices connected to a Linky Energy Meter.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atome/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/atome/manifest.json new file mode 100644 index 00000000000..975e7f1ac31 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atome/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "atome", + "name": "Atome Linky", + "documentation": "https://www.home-assistant.io/integrations/atome", + "codeowners": ["@baqs"], + "requirements": ["pyatome==0.1.1"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/atome/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/atome/sensor.py new file mode 100644 index 00000000000..d10024f64c2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/atome/sensor.py @@ -0,0 +1,277 @@ +"""Linky Atome.""" +from datetime import timedelta +import logging + +from pyatome.client import AtomeClient, PyAtomeError +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.const import ( + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + DEVICE_CLASS_POWER, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "atome" + +LIVE_SCAN_INTERVAL = timedelta(seconds=30) +DAILY_SCAN_INTERVAL = timedelta(seconds=150) +WEEKLY_SCAN_INTERVAL = timedelta(hours=1) +MONTHLY_SCAN_INTERVAL = timedelta(hours=1) +YEARLY_SCAN_INTERVAL = timedelta(days=1) + +LIVE_NAME = "Atome Live Power" +DAILY_NAME = "Atome Daily" +WEEKLY_NAME = "Atome Weekly" +MONTHLY_NAME = "Atome Monthly" +YEARLY_NAME = "Atome Yearly" + +LIVE_TYPE = "live" +DAILY_TYPE = "day" +WEEKLY_TYPE = "week" +MONTHLY_TYPE = "month" +YEARLY_TYPE = "year" + +ICON = "mdi:flash" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Atome sensor.""" + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + + try: + atome_client = AtomeClient(username, password) + atome_client.login() + except PyAtomeError as exp: + _LOGGER.error(exp) + return + + data = AtomeData(atome_client) + + sensors = [] + sensors.append(AtomeSensor(data, LIVE_NAME, LIVE_TYPE)) + sensors.append(AtomeSensor(data, DAILY_NAME, DAILY_TYPE)) + sensors.append(AtomeSensor(data, WEEKLY_NAME, WEEKLY_TYPE)) + sensors.append(AtomeSensor(data, MONTHLY_NAME, MONTHLY_TYPE)) + sensors.append(AtomeSensor(data, YEARLY_NAME, YEARLY_TYPE)) + + add_entities(sensors, True) + + +class AtomeData: + """Stores data retrieved from Neurio sensor.""" + + def __init__(self, client: AtomeClient): + """Initialize the data.""" + self.atome_client = client + self._live_power = None + self._subscribed_power = None + self._is_connected = None + self._day_usage = None + self._day_price = None + self._week_usage = None + self._week_price = None + self._month_usage = None + self._month_price = None + self._year_usage = None + self._year_price = None + + @property + def live_power(self): + """Return latest active power value.""" + return self._live_power + + @property + def subscribed_power(self): + """Return latest active power value.""" + return self._subscribed_power + + @property + def is_connected(self): + """Return latest active power value.""" + return self._is_connected + + @Throttle(LIVE_SCAN_INTERVAL) + def update_live_usage(self): + """Return current power value.""" + try: + values = self.atome_client.get_live() + self._live_power = values["last"] + self._subscribed_power = values["subscribed"] + self._is_connected = values["isConnected"] + _LOGGER.debug( + "Updating Atome live data. Got: %d, isConnected: %s, subscribed: %d", + self._live_power, + self._is_connected, + self._subscribed_power, + ) + + except KeyError as error: + _LOGGER.error("Missing last value in values: %s: %s", values, error) + + @property + def day_usage(self): + """Return latest daily usage value.""" + return self._day_usage + + @property + def day_price(self): + """Return latest daily usage value.""" + return self._day_price + + @Throttle(DAILY_SCAN_INTERVAL) + def update_day_usage(self): + """Return current daily power usage.""" + try: + values = self.atome_client.get_consumption(DAILY_TYPE) + self._day_usage = values["total"] / 1000 + self._day_price = values["price"] + _LOGGER.debug("Updating Atome daily data. Got: %d", self._day_usage) + + except KeyError as error: + _LOGGER.error("Missing last value in values: %s: %s", values, error) + + @property + def week_usage(self): + """Return latest weekly usage value.""" + return self._week_usage + + @property + def week_price(self): + """Return latest weekly usage value.""" + return self._week_price + + @Throttle(WEEKLY_SCAN_INTERVAL) + def update_week_usage(self): + """Return current weekly power usage.""" + try: + values = self.atome_client.get_consumption(WEEKLY_TYPE) + self._week_usage = values["total"] / 1000 + self._week_price = values["price"] + _LOGGER.debug("Updating Atome weekly data. Got: %d", self._week_usage) + + except KeyError as error: + _LOGGER.error("Missing last value in values: %s: %s", values, error) + + @property + def month_usage(self): + """Return latest monthly usage value.""" + return self._month_usage + + @property + def month_price(self): + """Return latest monthly usage value.""" + return self._month_price + + @Throttle(MONTHLY_SCAN_INTERVAL) + def update_month_usage(self): + """Return current monthly power usage.""" + try: + values = self.atome_client.get_consumption(MONTHLY_TYPE) + self._month_usage = values["total"] / 1000 + self._month_price = values["price"] + _LOGGER.debug("Updating Atome monthly data. Got: %d", self._month_usage) + + except KeyError as error: + _LOGGER.error("Missing last value in values: %s: %s", values, error) + + @property + def year_usage(self): + """Return latest yearly usage value.""" + return self._year_usage + + @property + def year_price(self): + """Return latest yearly usage value.""" + return self._year_price + + @Throttle(YEARLY_SCAN_INTERVAL) + def update_year_usage(self): + """Return current yearly power usage.""" + try: + values = self.atome_client.get_consumption(YEARLY_TYPE) + self._year_usage = values["total"] / 1000 + self._year_price = values["price"] + _LOGGER.debug("Updating Atome yearly data. Got: %d", self._year_usage) + + except KeyError as error: + _LOGGER.error("Missing last value in values: %s: %s", values, error) + + +class AtomeSensor(SensorEntity): + """Representation of a sensor entity for Atome.""" + + def __init__(self, data, name, sensor_type): + """Initialize the sensor.""" + self._name = name + self._data = data + self._state = None + self._attributes = {} + + self._sensor_type = sensor_type + + if sensor_type == LIVE_TYPE: + self._unit_of_measurement = POWER_WATT + else: + self._unit_of_measurement = ENERGY_KILO_WATT_HOUR + + @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._state + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return ICON + + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_POWER + + def update(self): + """Update device state.""" + update_function = getattr(self._data, f"update_{self._sensor_type}_usage") + update_function() + + if self._sensor_type == LIVE_TYPE: + self._state = self._data.live_power + self._attributes["subscribed_power"] = self._data.subscribed_power + self._attributes["is_connected"] = self._data.is_connected + else: + self._state = getattr(self._data, f"{self._sensor_type}_usage") + self._attributes["price"] = getattr( + self._data, f"{self._sensor_type}_price" + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/august/__init__.py new file mode 100644 index 00000000000..30374dcb220 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/__init__.py @@ -0,0 +1,330 @@ +"""Support for August devices.""" +import asyncio +from itertools import chain +import logging + +from aiohttp import ClientError, ClientResponseError +from yalexs.exceptions import AugustApiAIOHTTPError +from yalexs.pubnub_activity import activities_from_pubnub_message +from yalexs.pubnub_async import AugustPubNub, async_create_pubnub + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) + +from .activity import ActivityStream +from .const import DATA_AUGUST, DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS +from .exceptions import CannotConnect, InvalidAuth, RequireValidation +from .gateway import AugustGateway +from .subscriber import AugustSubscriberMixin + +_LOGGER = logging.getLogger(__name__) + +API_CACHED_ATTRS = ( + "door_state", + "door_state_datetime", + "lock_status", + "lock_status_datetime", +) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up August from a config entry.""" + + august_gateway = AugustGateway(hass) + + try: + await august_gateway.async_setup(entry.data) + return await async_setup_august(hass, entry, august_gateway) + except (RequireValidation, InvalidAuth) as err: + raise ConfigEntryAuthFailed from err + except (ClientResponseError, CannotConnect, asyncio.TimeoutError) as err: + raise ConfigEntryNotReady from err + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + + hass.data[DOMAIN][entry.entry_id][DATA_AUGUST].async_stop() + + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +async def async_setup_august(hass, config_entry, august_gateway): + """Set up the August component.""" + + if CONF_PASSWORD in config_entry.data: + # We no longer need to store passwords since we do not + # support YAML anymore + config_data = config_entry.data.copy() + del config_data[CONF_PASSWORD] + hass.config_entries.async_update_entry(config_entry, data=config_data) + + await august_gateway.async_authenticate() + + hass.data.setdefault(DOMAIN, {}) + data = hass.data[DOMAIN][config_entry.entry_id] = { + DATA_AUGUST: AugustData(hass, august_gateway) + } + await data[DATA_AUGUST].async_setup() + + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + + return True + + +class AugustData(AugustSubscriberMixin): + """August data object.""" + + def __init__(self, hass, august_gateway): + """Init August data object.""" + super().__init__(hass, MIN_TIME_BETWEEN_DETAIL_UPDATES) + self._hass = hass + self._august_gateway = august_gateway + self.activity_stream = None + self._api = august_gateway.api + self._device_detail_by_id = {} + self._doorbells_by_id = {} + self._locks_by_id = {} + self._house_ids = set() + self._pubnub_unsub = None + + async def async_setup(self): + """Async setup of august device data and activities.""" + token = self._august_gateway.access_token + user_data, locks, doorbells = await asyncio.gather( + self._api.async_get_user(token), + self._api.async_get_operable_locks(token), + self._api.async_get_doorbells(token), + ) + if not doorbells: + doorbells = [] + if not locks: + locks = [] + + self._doorbells_by_id = {device.device_id: device for device in doorbells} + self._locks_by_id = {device.device_id: device for device in locks} + self._house_ids = {device.house_id for device in chain(locks, doorbells)} + + await self._async_refresh_device_detail_by_ids( + [device.device_id for device in chain(locks, doorbells)] + ) + + # We remove all devices that we are missing + # detail as we cannot determine if they are usable. + # This also allows us to avoid checking for + # detail being None all over the place + self._remove_inoperative_locks() + self._remove_inoperative_doorbells() + + pubnub = AugustPubNub() + for device in self._device_detail_by_id.values(): + pubnub.register_device(device) + + self.activity_stream = ActivityStream( + self._hass, self._api, self._august_gateway, self._house_ids, pubnub + ) + await self.activity_stream.async_setup() + pubnub.subscribe(self.async_pubnub_message) + self._pubnub_unsub = async_create_pubnub(user_data["UserID"], pubnub) + + @callback + def async_pubnub_message(self, device_id, date_time, message): + """Process a pubnub message.""" + device = self.get_device_detail(device_id) + activities = activities_from_pubnub_message(device, date_time, message) + if activities: + self.activity_stream.async_process_newer_device_activities(activities) + self.async_signal_device_id_update(device.device_id) + self.activity_stream.async_schedule_house_id_refresh(device.house_id) + + @callback + def async_stop(self): + """Stop the subscriptions.""" + self._pubnub_unsub() + self.activity_stream.async_stop() + + @property + def doorbells(self): + """Return a list of py-august Doorbell objects.""" + return self._doorbells_by_id.values() + + @property + def locks(self): + """Return a list of py-august Lock objects.""" + return self._locks_by_id.values() + + def get_device_detail(self, device_id): + """Return the py-august LockDetail or DoorbellDetail object for a device.""" + return self._device_detail_by_id[device_id] + + async def _async_refresh(self, time): + await self._async_refresh_device_detail_by_ids(self._subscriptions.keys()) + + async def _async_refresh_device_detail_by_ids(self, device_ids_list): + await asyncio.gather( + *[ + self._async_refresh_device_detail_by_id(device_id) + for device_id in device_ids_list + ] + ) + + async def _async_refresh_device_detail_by_id(self, device_id): + if device_id in self._locks_by_id: + if self.activity_stream and self.activity_stream.pubnub.connected: + saved_attrs = _save_live_attrs(self._device_detail_by_id[device_id]) + await self._async_update_device_detail( + self._locks_by_id[device_id], self._api.async_get_lock_detail + ) + if self.activity_stream and self.activity_stream.pubnub.connected: + _restore_live_attrs(self._device_detail_by_id[device_id], saved_attrs) + # keypads are always attached to locks + if ( + device_id in self._device_detail_by_id + and self._device_detail_by_id[device_id].keypad is not None + ): + keypad = self._device_detail_by_id[device_id].keypad + self._device_detail_by_id[keypad.device_id] = keypad + elif device_id in self._doorbells_by_id: + await self._async_update_device_detail( + self._doorbells_by_id[device_id], + self._api.async_get_doorbell_detail, + ) + _LOGGER.debug( + "async_signal_device_id_update (from detail updates): %s", device_id + ) + self.async_signal_device_id_update(device_id) + + async def _async_update_device_detail(self, device, api_call): + _LOGGER.debug( + "Started retrieving detail for %s (%s)", + device.device_name, + device.device_id, + ) + + try: + self._device_detail_by_id[device.device_id] = await api_call( + self._august_gateway.access_token, device.device_id + ) + except ClientError as ex: + _LOGGER.error( + "Request error trying to retrieve %s details for %s. %s", + device.device_id, + device.device_name, + ex, + ) + _LOGGER.debug( + "Completed retrieving detail for %s (%s)", + device.device_name, + device.device_id, + ) + + def _get_device_name(self, device_id): + """Return doorbell or lock name as August has it stored.""" + if device_id in self._locks_by_id: + return self._locks_by_id[device_id].device_name + if device_id in self._doorbells_by_id: + return self._doorbells_by_id[device_id].device_name + + async def async_lock(self, device_id): + """Lock the device.""" + return await self._async_call_api_op_requires_bridge( + device_id, + self._api.async_lock_return_activities, + self._august_gateway.access_token, + device_id, + ) + + async def async_unlock(self, device_id): + """Unlock the device.""" + return await self._async_call_api_op_requires_bridge( + device_id, + self._api.async_unlock_return_activities, + self._august_gateway.access_token, + device_id, + ) + + async def _async_call_api_op_requires_bridge( + self, device_id, func, *args, **kwargs + ): + """Call an API that requires the bridge to be online and will change the device state.""" + ret = None + try: + ret = await func(*args, **kwargs) + except AugustApiAIOHTTPError as err: + device_name = self._get_device_name(device_id) + if device_name is None: + device_name = f"DeviceID: {device_id}" + raise HomeAssistantError(f"{device_name}: {err}") from err + + return ret + + def _remove_inoperative_doorbells(self): + for doorbell in list(self.doorbells): + device_id = doorbell.device_id + doorbell_is_operative = False + doorbell_detail = self._device_detail_by_id.get(device_id) + if doorbell_detail is None: + _LOGGER.info( + "The doorbell %s could not be setup because the system could not fetch details about the doorbell", + doorbell.device_name, + ) + else: + doorbell_is_operative = True + + if not doorbell_is_operative: + del self._doorbells_by_id[device_id] + del self._device_detail_by_id[device_id] + + def _remove_inoperative_locks(self): + # Remove non-operative locks as there must + # be a bridge (August Connect) for them to + # be usable + for lock in list(self.locks): + device_id = lock.device_id + lock_is_operative = False + lock_detail = self._device_detail_by_id.get(device_id) + if lock_detail is None: + _LOGGER.info( + "The lock %s could not be setup because the system could not fetch details about the lock", + lock.device_name, + ) + elif lock_detail.bridge is None: + _LOGGER.info( + "The lock %s could not be setup because it does not have a bridge (Connect)", + lock.device_name, + ) + # Bridge may come back online later so we still add the device since we will + # have a pubnub subscription to tell use when it recovers + else: + lock_is_operative = True + + if not lock_is_operative: + del self._locks_by_id[device_id] + del self._device_detail_by_id[device_id] + + +def _save_live_attrs(lock_detail): + """Store the attributes that the lock detail api may have an invalid cache for. + + Since we are connected to pubnub we may have more current data + then the api so we want to restore the most current data after + updating battery state etc. + """ + return {attr: getattr(lock_detail, attr) for attr in API_CACHED_ATTRS} + + +def _restore_live_attrs(lock_detail, attrs): + """Restore the non-cache attributes after a cached update.""" + for attr, value in attrs.items(): + setattr(lock_detail, attr, value) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/activity.py b/homeassistant-2021.6.0.dev0/homeassistant/components/august/activity.py new file mode 100644 index 00000000000..402a2ccd610 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/activity.py @@ -0,0 +1,185 @@ +"""Consume the august activity stream.""" +import asyncio +import logging + +from aiohttp import ClientError + +from homeassistant.core import callback +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.event import async_call_later +from homeassistant.util.dt import utcnow + +from .const import ACTIVITY_UPDATE_INTERVAL +from .subscriber import AugustSubscriberMixin + +_LOGGER = logging.getLogger(__name__) + +ACTIVITY_STREAM_FETCH_LIMIT = 10 +ACTIVITY_CATCH_UP_FETCH_LIMIT = 2500 + + +class ActivityStream(AugustSubscriberMixin): + """August activity stream handler.""" + + def __init__(self, hass, api, august_gateway, house_ids, pubnub): + """Init August activity stream object.""" + super().__init__(hass, ACTIVITY_UPDATE_INTERVAL) + self._hass = hass + self._schedule_updates = {} + self._august_gateway = august_gateway + self._api = api + self._house_ids = house_ids + self._latest_activities = {} + self._last_update_time = None + self.pubnub = pubnub + self._update_debounce = {} + + async def async_setup(self): + """Token refresh check and catch up the activity stream.""" + for house_id in self._house_ids: + self._update_debounce[house_id] = self._async_create_debouncer(house_id) + + await self._async_refresh(utcnow()) + + @callback + def _async_create_debouncer(self, house_id): + """Create a debouncer for the house id.""" + + async def _async_update_house_id(): + await self._async_update_house_id(house_id) + + return Debouncer( + self._hass, + _LOGGER, + cooldown=ACTIVITY_UPDATE_INTERVAL.total_seconds(), + immediate=True, + function=_async_update_house_id, + ) + + @callback + def async_stop(self): + """Cleanup any debounces.""" + for debouncer in self._update_debounce.values(): + debouncer.async_cancel() + for house_id in self._schedule_updates: + if self._schedule_updates[house_id] is not None: + self._schedule_updates[house_id]() + self._schedule_updates[house_id] = None + + def get_latest_device_activity(self, device_id, activity_types): + """Return latest activity that is one of the acitivty_types.""" + if device_id not in self._latest_activities: + return None + + latest_device_activities = self._latest_activities[device_id] + latest_activity = None + + for activity_type in activity_types: + if activity_type in latest_device_activities: + if ( + latest_activity is not None + and latest_device_activities[activity_type].activity_start_time + <= latest_activity.activity_start_time + ): + continue + latest_activity = latest_device_activities[activity_type] + + return latest_activity + + async def _async_refresh(self, time): + """Update the activity stream from August.""" + # This is the only place we refresh the api token + await self._august_gateway.async_refresh_access_token_if_needed() + if self.pubnub.connected: + _LOGGER.debug("Skipping update because pubnub is connected") + return + await self._async_update_device_activities(time) + + async def _async_update_device_activities(self, time): + _LOGGER.debug("Start retrieving device activities") + await asyncio.gather( + *[ + self._update_debounce[house_id].async_call() + for house_id in self._house_ids + ] + ) + self._last_update_time = time + + @callback + def async_schedule_house_id_refresh(self, house_id): + """Update for a house activities now and once in the future.""" + if self._schedule_updates.get(house_id): + self._schedule_updates[house_id]() + self._schedule_updates[house_id] = None + + async def _update_house_activities(_): + await self._update_debounce[house_id].async_call() + + self._hass.async_create_task(self._update_debounce[house_id].async_call()) + # Schedule an update past the debounce to ensure + # we catch the case where the lock operator is + # not updated or the lock failed + self._schedule_updates[house_id] = async_call_later( + self._hass, + ACTIVITY_UPDATE_INTERVAL.total_seconds() + 1, + _update_house_activities, + ) + + async def _async_update_house_id(self, house_id): + """Update device activities for a house.""" + if self._last_update_time: + limit = ACTIVITY_STREAM_FETCH_LIMIT + else: + limit = ACTIVITY_CATCH_UP_FETCH_LIMIT + + _LOGGER.debug("Updating device activity for house id %s", house_id) + try: + activities = await self._api.async_get_house_activities( + self._august_gateway.access_token, house_id, limit=limit + ) + except ClientError as ex: + _LOGGER.error( + "Request error trying to retrieve activity for house id %s: %s", + house_id, + ex, + ) + # Make sure we process the next house if one of them fails + return + + _LOGGER.debug( + "Completed retrieving device activities for house id %s", house_id + ) + + updated_device_ids = self.async_process_newer_device_activities(activities) + + if not updated_device_ids: + return + + for device_id in updated_device_ids: + _LOGGER.debug( + "async_signal_device_id_update (from activity stream): %s", + device_id, + ) + self.async_signal_device_id_update(device_id) + + def async_process_newer_device_activities(self, activities): + """Process activities if they are newer than the last one.""" + updated_device_ids = set() + for activity in activities: + device_id = activity.device_id + activity_type = activity.activity_type + device_activities = self._latest_activities.setdefault(device_id, {}) + lastest_activity = device_activities.get(activity_type) + + # Ignore activities that are older than the latest one + if ( + lastest_activity + and lastest_activity.activity_start_time >= activity.activity_start_time + ): + continue + + device_activities[activity_type] = activity + + updated_device_ids.add(device_id) + + return updated_device_ids diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/binary_sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/august/binary_sensor.py new file mode 100644 index 00000000000..e72d4b186a5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/binary_sensor.py @@ -0,0 +1,284 @@ +"""Support for August binary sensors.""" +from datetime import datetime, timedelta +import logging + +from yalexs.activity import ACTION_DOORBELL_CALL_MISSED, SOURCE_PUBNUB, ActivityType +from yalexs.lock import LockDoorStatus +from yalexs.util import update_lock_detail_from_activity + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_OCCUPANCY, + BinarySensorEntity, +) +from homeassistant.core import callback +from homeassistant.helpers.event import async_call_later + +from .const import ACTIVITY_UPDATE_INTERVAL, DATA_AUGUST, DOMAIN +from .entity import AugustEntityMixin + +_LOGGER = logging.getLogger(__name__) + +TIME_TO_DECLARE_DETECTION = timedelta(seconds=ACTIVITY_UPDATE_INTERVAL.total_seconds()) +TIME_TO_RECHECK_DETECTION = timedelta( + seconds=ACTIVITY_UPDATE_INTERVAL.total_seconds() * 3 +) + + +def _retrieve_online_state(data, detail): + """Get the latest state of the sensor.""" + # The doorbell will go into standby mode when there is no motion + # for a short while. It will wake by itself when needed so we need + # to consider is available or we will not report motion or dings + + return detail.is_online or detail.is_standby + + +def _retrieve_motion_state(data, detail): + latest = data.activity_stream.get_latest_device_activity( + detail.device_id, {ActivityType.DOORBELL_MOTION} + ) + + if latest is None: + return False + + return _activity_time_based_state(latest) + + +def _retrieve_ding_state(data, detail): + latest = data.activity_stream.get_latest_device_activity( + detail.device_id, {ActivityType.DOORBELL_DING} + ) + + if latest is None: + return False + + if ( + data.activity_stream.pubnub.connected + and latest.action == ACTION_DOORBELL_CALL_MISSED + ): + return False + + return _activity_time_based_state(latest) + + +def _activity_time_based_state(latest): + """Get the latest state of the sensor.""" + start = latest.activity_start_time + end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION + return start <= _native_datetime() <= end + + +def _native_datetime(): + """Return time in the format august uses without timezone.""" + return datetime.now() + + +SENSOR_NAME = 0 +SENSOR_DEVICE_CLASS = 1 +SENSOR_STATE_PROVIDER = 2 +SENSOR_STATE_IS_TIME_BASED = 3 + +# sensor_type: [name, device_class, state_provider, is_time_based] +SENSOR_TYPES_DOORBELL = { + "doorbell_ding": ["Ding", DEVICE_CLASS_OCCUPANCY, _retrieve_ding_state, True], + "doorbell_motion": ["Motion", DEVICE_CLASS_MOTION, _retrieve_motion_state, True], + "doorbell_online": [ + "Online", + DEVICE_CLASS_CONNECTIVITY, + _retrieve_online_state, + False, + ], +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the August binary sensors.""" + data = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] + entities = [] + + for door in data.locks: + detail = data.get_device_detail(door.device_id) + if not detail.doorsense: + _LOGGER.debug( + "Not adding sensor class door for lock %s because it does not have doorsense", + door.device_name, + ) + continue + + _LOGGER.debug("Adding sensor class door for %s", door.device_name) + entities.append(AugustDoorBinarySensor(data, "door_open", door)) + + for doorbell in data.doorbells: + for sensor_type in SENSOR_TYPES_DOORBELL: + _LOGGER.debug( + "Adding doorbell sensor class %s for %s", + SENSOR_TYPES_DOORBELL[sensor_type][SENSOR_DEVICE_CLASS], + doorbell.device_name, + ) + entities.append(AugustDoorbellBinarySensor(data, sensor_type, doorbell)) + + async_add_entities(entities) + + +class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity): + """Representation of an August Door binary sensor.""" + + def __init__(self, data, sensor_type, device): + """Initialize the sensor.""" + super().__init__(data, device) + self._data = data + self._sensor_type = sensor_type + self._device = device + self._update_from_data() + + @property + def available(self): + """Return the availability of this sensor.""" + return self._detail.bridge_is_online + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._detail.door_state == LockDoorStatus.OPEN + + @property + def device_class(self): + """Return the class of this device.""" + return DEVICE_CLASS_DOOR + + @property + def name(self): + """Return the name of the binary sensor.""" + return f"{self._device.device_name} Open" + + @callback + def _update_from_data(self): + """Get the latest state of the sensor and update activity.""" + door_activity = self._data.activity_stream.get_latest_device_activity( + self._device_id, {ActivityType.DOOR_OPERATION} + ) + + if door_activity is not None: + update_lock_detail_from_activity(self._detail, door_activity) + # If the source is pubnub the lock must be online since its a live update + if door_activity.source == SOURCE_PUBNUB: + self._detail.set_online(True) + + bridge_activity = self._data.activity_stream.get_latest_device_activity( + self._device_id, {ActivityType.BRIDGE_OPERATION} + ) + + if bridge_activity is not None: + update_lock_detail_from_activity(self._detail, bridge_activity) + + @property + def unique_id(self) -> str: + """Get the unique of the door open binary sensor.""" + return f"{self._device_id}_open" + + +class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): + """Representation of an August binary sensor.""" + + def __init__(self, data, sensor_type, device): + """Initialize the sensor.""" + super().__init__(data, device) + self._check_for_off_update_listener = None + self._data = data + self._sensor_type = sensor_type + self._device = device + self._state = None + self._available = False + self._update_from_data() + + @property + def available(self): + """Return the availability of this sensor.""" + return self._available + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + @property + def _sensor_config(self): + """Return the config for the sensor.""" + return SENSOR_TYPES_DOORBELL[self._sensor_type] + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return self._sensor_config[SENSOR_DEVICE_CLASS] + + @property + def name(self): + """Return the name of the binary sensor.""" + return f"{self._device.device_name} {self._sensor_config[SENSOR_NAME]}" + + @property + def _state_provider(self): + """Return the state provider for the binary sensor.""" + return self._sensor_config[SENSOR_STATE_PROVIDER] + + @property + def _is_time_based(self): + """Return true of false if the sensor is time based.""" + return self._sensor_config[SENSOR_STATE_IS_TIME_BASED] + + @callback + def _update_from_data(self): + """Get the latest state of the sensor.""" + self._cancel_any_pending_updates() + self._state = self._state_provider(self._data, self._detail) + + if self._is_time_based: + self._available = _retrieve_online_state(self._data, self._detail) + self._schedule_update_to_recheck_turn_off_sensor() + else: + self._available = True + + def _schedule_update_to_recheck_turn_off_sensor(self): + """Schedule an update to recheck the sensor to see if it is ready to turn off.""" + + # If the sensor is already off there is nothing to do + if not self._state: + return + + # self.hass is only available after setup is completed + # and we will recheck in async_added_to_hass + if not self.hass: + return + + @callback + def _scheduled_update(now): + """Timer callback for sensor update.""" + self._check_for_off_update_listener = None + self._update_from_data() + if not self._state: + self.async_write_ha_state() + + self._check_for_off_update_listener = async_call_later( + self.hass, TIME_TO_RECHECK_DETECTION.total_seconds(), _scheduled_update + ) + + def _cancel_any_pending_updates(self): + """Cancel any updates to recheck a sensor to see if it is ready to turn off.""" + if not self._check_for_off_update_listener: + return + _LOGGER.debug("%s: canceled pending update", self.entity_id) + self._check_for_off_update_listener() + self._check_for_off_update_listener = None + + async def async_added_to_hass(self): + """Call the mixin to subscribe and setup an async_track_point_in_utc_time to turn off the sensor if needed.""" + self._schedule_update_to_recheck_turn_off_sensor() + await super().async_added_to_hass() + + @property + def unique_id(self) -> str: + """Get the unique id of the doorbell sensor.""" + return f"{self._device_id}_{self._sensor_config[SENSOR_NAME].lower()}" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/camera.py b/homeassistant-2021.6.0.dev0/homeassistant/components/august/camera.py new file mode 100644 index 00000000000..daaa7624aa3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/camera.py @@ -0,0 +1,88 @@ +"""Support for August doorbell camera.""" + +from yalexs.activity import ActivityType +from yalexs.util import update_doorbell_image_from_activity + +from homeassistant.components.camera import Camera +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client + +from .const import DATA_AUGUST, DEFAULT_NAME, DEFAULT_TIMEOUT, DOMAIN +from .entity import AugustEntityMixin + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up August cameras.""" + data = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] + session = aiohttp_client.async_get_clientsession(hass) + async_add_entities( + [ + AugustCamera(data, doorbell, session, DEFAULT_TIMEOUT) + for doorbell in data.doorbells + ] + ) + + +class AugustCamera(AugustEntityMixin, Camera): + """An implementation of a August security camera.""" + + def __init__(self, data, device, session, timeout): + """Initialize a August security camera.""" + super().__init__(data, device) + self._data = data + self._device = device + self._timeout = timeout + self._session = session + self._image_url = None + self._image_content = None + + @property + def name(self): + """Return the name of this device.""" + return f"{self._device.device_name} Camera" + + @property + def is_recording(self): + """Return true if the device is recording.""" + return self._device.has_subscription + + @property + def motion_detection_enabled(self): + """Return the camera motion detection status.""" + return True + + @property + def brand(self): + """Return the camera brand.""" + return DEFAULT_NAME + + @property + def model(self): + """Return the camera model.""" + return self._detail.model + + @callback + def _update_from_data(self): + """Get the latest state of the sensor.""" + doorbell_activity = self._data.activity_stream.get_latest_device_activity( + self._device_id, {ActivityType.DOORBELL_MOTION} + ) + + if doorbell_activity is not None: + update_doorbell_image_from_activity(self._detail, doorbell_activity) + + async def async_camera_image(self): + """Return bytes of camera image.""" + self._update_from_data() + + if self._image_url is not self._detail.image_url: + self._image_url = self._detail.image_url + self._image_content = await self._detail.async_get_doorbell_image( + self._session, timeout=self._timeout + ) + return self._image_content + + @property + def unique_id(self) -> str: + """Get the unique id of the camera.""" + return f"{self._device_id:s}_camera" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/august/config_flow.py new file mode 100644 index 00000000000..af048f9dc46 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/config_flow.py @@ -0,0 +1,179 @@ +"""Config flow for August integration.""" +import logging + +import voluptuous as vol +from yalexs.authenticator import ValidationResult + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from .const import CONF_LOGIN_METHOD, DOMAIN, LOGIN_METHODS, VERIFICATION_CODE_KEY +from .exceptions import CannotConnect, InvalidAuth, RequireValidation +from .gateway import AugustGateway + +_LOGGER = logging.getLogger(__name__) + + +async def async_validate_input( + data, + august_gateway, +): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + + Request configuration steps from the user. + """ + + code = data.get(VERIFICATION_CODE_KEY) + + if code is not None: + result = await august_gateway.authenticator.async_validate_verification_code( + code + ) + _LOGGER.debug("Verification code validation: %s", result) + if result != ValidationResult.VALIDATED: + raise RequireValidation + + try: + await august_gateway.async_authenticate() + except RequireValidation: + _LOGGER.debug( + "Requesting new verification code for %s via %s", + data.get(CONF_USERNAME), + data.get(CONF_LOGIN_METHOD), + ) + if code is None: + await august_gateway.authenticator.async_send_verification_code() + raise + + return { + "title": data.get(CONF_USERNAME), + "data": august_gateway.config_entry(), + } + + +class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for August.""" + + VERSION = 1 + + def __init__(self): + """Store an AugustGateway().""" + self._august_gateway = None + self._user_auth_details = {} + self._needs_reset = False + self._mode = None + super().__init__() + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + self._august_gateway = AugustGateway(self.hass) + return await self.async_step_user_validate() + + async def async_step_user_validate(self, user_input=None): + """Handle authentication.""" + errors = {} + if user_input is not None: + result = await self._async_auth_or_validate(user_input, errors) + if result is not None: + return result + + return self.async_show_form( + step_id="user_validate", + data_schema=vol.Schema( + { + vol.Required( + CONF_LOGIN_METHOD, + default=self._user_auth_details.get(CONF_LOGIN_METHOD, "phone"), + ): vol.In(LOGIN_METHODS), + vol.Required( + CONF_USERNAME, + default=self._user_auth_details.get(CONF_USERNAME), + ): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) + + async def async_step_validation(self, user_input=None): + """Handle validation (2fa) step.""" + if user_input: + if self._mode == "reauth": + return await self.async_step_reauth_validate(user_input) + return await self.async_step_user_validate(user_input) + + return self.async_show_form( + step_id="validation", + data_schema=vol.Schema( + {vol.Required(VERIFICATION_CODE_KEY): vol.All(str, vol.Strip)} + ), + description_placeholders={ + CONF_USERNAME: self._user_auth_details[CONF_USERNAME], + CONF_LOGIN_METHOD: self._user_auth_details[CONF_LOGIN_METHOD], + }, + ) + + async def async_step_reauth(self, data): + """Handle configuration by re-auth.""" + self._user_auth_details = dict(data) + self._mode = "reauth" + self._needs_reset = True + self._august_gateway = AugustGateway(self.hass) + return await self.async_step_reauth_validate() + + async def async_step_reauth_validate(self, user_input=None): + """Handle reauth and validation.""" + errors = {} + if user_input is not None: + result = await self._async_auth_or_validate(user_input, errors) + if result is not None: + return result + + return self.async_show_form( + step_id="reauth_validate", + data_schema=vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + description_placeholders={ + CONF_USERNAME: self._user_auth_details[CONF_USERNAME], + }, + ) + + async def _async_auth_or_validate(self, user_input, errors): + self._user_auth_details.update(user_input) + await self._august_gateway.async_setup(self._user_auth_details) + if self._needs_reset: + self._needs_reset = False + await self._august_gateway.async_reset_authentication() + try: + info = await async_validate_input( + self._user_auth_details, + self._august_gateway, + ) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except RequireValidation: + return await self.async_step_validation() + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if errors: + return None + + existing_entry = await self.async_set_unique_id( + self._user_auth_details[CONF_USERNAME] + ) + if not existing_entry: + return self.async_create_entry(title=info["title"], data=info["data"]) + + self.hass.config_entries.async_update_entry(existing_entry, data=info["data"]) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/august/const.py new file mode 100644 index 00000000000..57e0d5a7fb7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/const.py @@ -0,0 +1,46 @@ +"""Constants for August devices.""" + +from datetime import timedelta + +DEFAULT_TIMEOUT = 10 + +CONF_ACCESS_TOKEN_CACHE_FILE = "access_token_cache_file" +CONF_LOGIN_METHOD = "login_method" +CONF_INSTALL_ID = "install_id" + +VERIFICATION_CODE_KEY = "verification_code" + +NOTIFICATION_ID = "august_notification" +NOTIFICATION_TITLE = "August" + +MANUFACTURER = "August Home Inc." + +DEFAULT_AUGUST_CONFIG_FILE = ".august.conf" + +DATA_AUGUST = "data_august" + +DEFAULT_NAME = "August" +DOMAIN = "august" + +OPERATION_METHOD_AUTORELOCK = "autorelock" +OPERATION_METHOD_REMOTE = "remote" +OPERATION_METHOD_KEYPAD = "keypad" +OPERATION_METHOD_MOBILE_DEVICE = "mobile" + +ATTR_OPERATION_AUTORELOCK = "autorelock" +ATTR_OPERATION_METHOD = "method" +ATTR_OPERATION_REMOTE = "remote" +ATTR_OPERATION_KEYPAD = "keypad" + +# Limit battery, online, and hardware updates to hourly +# in order to reduce the number of api requests and +# avoid hitting rate limits +MIN_TIME_BETWEEN_DETAIL_UPDATES = timedelta(hours=1) + +# Activity needs to be checked more frequently as the +# doorbell motion and rings are included here +ACTIVITY_UPDATE_INTERVAL = timedelta(seconds=10) + +LOGIN_METHODS = ["phone", "email"] + +PLATFORMS = ["camera", "binary_sensor", "lock", "sensor"] diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/entity.py b/homeassistant-2021.6.0.dev0/homeassistant/components/august/entity.py new file mode 100644 index 00000000000..b2a93948449 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/entity.py @@ -0,0 +1,78 @@ +"""Base class for August entity.""" +from homeassistant.core import callback +from homeassistant.helpers.entity import Entity + +from . import DOMAIN +from .const import MANUFACTURER + +DEVICE_TYPES = ["keypad", "lock", "camera", "doorbell", "door", "bell"] + + +class AugustEntityMixin(Entity): + """Base implementation for August device.""" + + def __init__(self, data, device): + """Initialize an August device.""" + super().__init__() + self._data = data + self._device = device + + @property + def should_poll(self): + """Return False, updates are controlled via the hub.""" + return False + + @property + def _device_id(self): + return self._device.device_id + + @property + def _detail(self): + return self._data.get_device_detail(self._device.device_id) + + @property + def device_info(self): + """Return the device_info of the device.""" + name = self._device.device_name + return { + "identifiers": {(DOMAIN, self._device_id)}, + "name": name, + "manufacturer": MANUFACTURER, + "sw_version": self._detail.firmware_version, + "model": self._detail.model, + "suggested_area": _remove_device_types(name, DEVICE_TYPES), + } + + @callback + def _update_from_data_and_write_state(self): + self._update_from_data() + self.async_write_ha_state() + + async def async_added_to_hass(self): + """Subscribe to updates.""" + self.async_on_remove( + self._data.async_subscribe_device_id( + self._device_id, self._update_from_data_and_write_state + ) + ) + self.async_on_remove( + self._data.activity_stream.async_subscribe_device_id( + self._device_id, self._update_from_data_and_write_state + ) + ) + + +def _remove_device_types(name, device_types): + """Strip device types from a string. + + August stores the name as Master Bed Lock + or Master Bed Door. We can come up with a + reasonable suggestion by removing the supported + device types from the string. + """ + lower_name = name.lower() + for device_type in device_types: + device_type_with_space = f" {device_type}" + if lower_name.endswith(device_type_with_space): + lower_name = lower_name[: -len(device_type_with_space)] + return name[: len(lower_name)] diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/exceptions.py b/homeassistant-2021.6.0.dev0/homeassistant/components/august/exceptions.py new file mode 100644 index 00000000000..edd418c9519 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/exceptions.py @@ -0,0 +1,15 @@ +"""Shared exceptions for the august integration.""" + +from homeassistant import exceptions + + +class RequireValidation(exceptions.HomeAssistantError): + """Error to indicate we require validation (2fa).""" + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/gateway.py b/homeassistant-2021.6.0.dev0/homeassistant/components/august/gateway.py new file mode 100644 index 00000000000..5499246a187 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/gateway.py @@ -0,0 +1,142 @@ +"""Handle August connection setup and authentication.""" + +import asyncio +import logging +import os + +from aiohttp import ClientError, ClientResponseError +from yalexs.api_async import ApiAsync +from yalexs.authenticator_async import AuthenticationState, AuthenticatorAsync + +from homeassistant.const import ( + CONF_PASSWORD, + CONF_TIMEOUT, + CONF_USERNAME, + HTTP_UNAUTHORIZED, +) +from homeassistant.helpers import aiohttp_client + +from .const import ( + CONF_ACCESS_TOKEN_CACHE_FILE, + CONF_INSTALL_ID, + CONF_LOGIN_METHOD, + DEFAULT_AUGUST_CONFIG_FILE, + DEFAULT_TIMEOUT, + VERIFICATION_CODE_KEY, +) +from .exceptions import CannotConnect, InvalidAuth, RequireValidation + +_LOGGER = logging.getLogger(__name__) + + +class AugustGateway: + """Handle the connection to August.""" + + def __init__(self, hass): + """Init the connection.""" + self._aiohttp_session = aiohttp_client.async_get_clientsession(hass) + self._token_refresh_lock = asyncio.Lock() + self._access_token_cache_file = None + self._hass = hass + self._config = None + self.api = None + self.authenticator = None + self.authentication = None + + @property + def access_token(self): + """Access token for the api.""" + return self.authentication.access_token + + def config_entry(self): + """Config entry.""" + return { + CONF_LOGIN_METHOD: self._config[CONF_LOGIN_METHOD], + CONF_USERNAME: self._config[CONF_USERNAME], + CONF_INSTALL_ID: self._config.get(CONF_INSTALL_ID), + CONF_ACCESS_TOKEN_CACHE_FILE: self._access_token_cache_file, + } + + async def async_setup(self, conf): + """Create the api and authenticator objects.""" + if conf.get(VERIFICATION_CODE_KEY): + return + + self._access_token_cache_file = conf.get( + CONF_ACCESS_TOKEN_CACHE_FILE, + f".{conf[CONF_USERNAME]}{DEFAULT_AUGUST_CONFIG_FILE}", + ) + self._config = conf + + self.api = ApiAsync( + self._aiohttp_session, + timeout=self._config.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), + ) + + self.authenticator = AuthenticatorAsync( + self.api, + self._config[CONF_LOGIN_METHOD], + self._config[CONF_USERNAME], + self._config.get(CONF_PASSWORD, ""), + install_id=self._config.get(CONF_INSTALL_ID), + access_token_cache_file=self._hass.config.path( + self._access_token_cache_file + ), + ) + + await self.authenticator.async_setup_authentication() + + async def async_authenticate(self): + """Authenticate with the details provided to setup.""" + self.authentication = None + try: + self.authentication = await self.authenticator.async_authenticate() + if self.authentication.state == AuthenticationState.AUTHENTICATED: + # Call the locks api to verify we are actually + # authenticated because we can be authenticated + # by have no access + await self.api.async_get_operable_locks(self.access_token) + except ClientResponseError as ex: + if ex.status == HTTP_UNAUTHORIZED: + raise InvalidAuth from ex + + raise CannotConnect from ex + except ClientError as ex: + _LOGGER.error("Unable to connect to August service: %s", str(ex)) + raise CannotConnect from ex + + if self.authentication.state == AuthenticationState.BAD_PASSWORD: + raise InvalidAuth + + if self.authentication.state == AuthenticationState.REQUIRES_VALIDATION: + raise RequireValidation + + if self.authentication.state != AuthenticationState.AUTHENTICATED: + _LOGGER.error("Unknown authentication state: %s", self.authentication.state) + raise InvalidAuth + + return self.authentication + + async def async_reset_authentication(self): + """Remove the cache file.""" + await self._hass.async_add_executor_job(self._reset_authentication) + + def _reset_authentication(self): + """Remove the cache file.""" + if os.path.exists(self._access_token_cache_file): + os.unlink(self._access_token_cache_file) + + async def async_refresh_access_token_if_needed(self): + """Refresh the august access token if needed.""" + if not self.authenticator.should_refresh(): + return + async with self._token_refresh_lock: + refreshed_authentication = ( + await self.authenticator.async_refresh_access_token(force=False) + ) + _LOGGER.info( + "Refreshed august access token. The old token expired at %s, and the new token expires at %s", + self.authentication.access_token_expires, + refreshed_authentication.access_token_expires, + ) + self.authentication = refreshed_authentication diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/lock.py b/homeassistant-2021.6.0.dev0/homeassistant/components/august/lock.py new file mode 100644 index 00000000000..6e4ee7e6f5c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/lock.py @@ -0,0 +1,136 @@ +"""Support for August lock.""" +import logging + +from yalexs.activity import SOURCE_PUBNUB, ActivityType +from yalexs.lock import LockStatus +from yalexs.util import update_lock_detail_from_activity + +from homeassistant.components.lock import ATTR_CHANGED_BY, LockEntity +from homeassistant.const import ATTR_BATTERY_LEVEL +from homeassistant.core import callback +from homeassistant.helpers.restore_state import RestoreEntity + +from .const import DATA_AUGUST, DOMAIN +from .entity import AugustEntityMixin + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up August locks.""" + data = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] + async_add_entities([AugustLock(data, lock) for lock in data.locks]) + + +class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): + """Representation of an August lock.""" + + def __init__(self, data, device): + """Initialize the lock.""" + super().__init__(data, device) + self._data = data + self._device = device + self._lock_status = None + self._changed_by = None + self._available = False + self._update_from_data() + + async def async_lock(self, **kwargs): + """Lock the device.""" + await self._call_lock_operation(self._data.async_lock) + + async def async_unlock(self, **kwargs): + """Unlock the device.""" + await self._call_lock_operation(self._data.async_unlock) + + async def _call_lock_operation(self, lock_operation): + activities = await lock_operation(self._device_id) + for lock_activity in activities: + update_lock_detail_from_activity(self._detail, lock_activity) + + if self._update_lock_status_from_detail(): + _LOGGER.debug( + "async_signal_device_id_update (from lock operation): %s", + self._device_id, + ) + self._data.async_signal_device_id_update(self._device_id) + + def _update_lock_status_from_detail(self): + self._available = self._detail.bridge_is_online + + if self._lock_status != self._detail.lock_status: + self._lock_status = self._detail.lock_status + return True + return False + + @callback + def _update_from_data(self): + """Get the latest state of the sensor and update activity.""" + lock_activity = self._data.activity_stream.get_latest_device_activity( + self._device_id, + {ActivityType.LOCK_OPERATION, ActivityType.LOCK_OPERATION_WITHOUT_OPERATOR}, + ) + + if lock_activity is not None: + self._changed_by = lock_activity.operated_by + update_lock_detail_from_activity(self._detail, lock_activity) + # If the source is pubnub the lock must be online since its a live update + if lock_activity.source == SOURCE_PUBNUB: + self._detail.set_online(True) + + bridge_activity = self._data.activity_stream.get_latest_device_activity( + self._device_id, {ActivityType.BRIDGE_OPERATION} + ) + + if bridge_activity is not None: + update_lock_detail_from_activity(self._detail, bridge_activity) + + self._update_lock_status_from_detail() + + @property + def name(self): + """Return the name of this device.""" + return self._device.device_name + + @property + def available(self): + """Return the availability of this sensor.""" + return self._available + + @property + def is_locked(self): + """Return true if device is on.""" + if self._lock_status is None or self._lock_status is LockStatus.UNKNOWN: + return None + return self._lock_status is LockStatus.LOCKED + + @property + def changed_by(self): + """Last change triggered by.""" + return self._changed_by + + @property + def extra_state_attributes(self): + """Return the device specific state attributes.""" + attributes = {ATTR_BATTERY_LEVEL: self._detail.battery_level} + + if self._detail.keypad is not None: + attributes["keypad_battery_level"] = self._detail.keypad.battery_level + + return attributes + + async def async_added_to_hass(self): + """Restore ATTR_CHANGED_BY on startup since it is likely no longer in the activity log.""" + await super().async_added_to_hass() + + last_state = await self.async_get_last_state() + if not last_state: + return + + if ATTR_CHANGED_BY in last_state.attributes: + self._changed_by = last_state.attributes[ATTR_CHANGED_BY] + + @property + def unique_id(self) -> str: + """Get the unique id of the lock.""" + return f"{self._device_id:s}_lock" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/manifest.json new file mode 100644 index 00000000000..e966338f287 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/manifest.json @@ -0,0 +1,23 @@ +{ + "domain": "august", + "name": "August", + "documentation": "https://www.home-assistant.io/integrations/august", + "requirements": ["yalexs==1.1.11"], + "codeowners": ["@bdraco"], + "dhcp": [ + { + "hostname": "connect", + "macaddress": "D86162*" + }, + { + "hostname": "connect", + "macaddress": "B8B7F1*" + }, + { + "hostname": "august*", + "macaddress": "E076D0*" + } + ], + "config_flow": true, + "iot_class": "cloud_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/august/sensor.py new file mode 100644 index 00000000000..1d973a83fc3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/sensor.py @@ -0,0 +1,274 @@ +"""Support for August sensors.""" +import logging + +from yalexs.activity import ActivityType + +from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, SensorEntity +from homeassistant.const import ATTR_ENTITY_PICTURE, PERCENTAGE, STATE_UNAVAILABLE +from homeassistant.core import callback +from homeassistant.helpers.entity_registry import async_get_registry +from homeassistant.helpers.restore_state import RestoreEntity + +from .const import ( + ATTR_OPERATION_AUTORELOCK, + ATTR_OPERATION_KEYPAD, + ATTR_OPERATION_METHOD, + ATTR_OPERATION_REMOTE, + DATA_AUGUST, + DOMAIN, + OPERATION_METHOD_AUTORELOCK, + OPERATION_METHOD_KEYPAD, + OPERATION_METHOD_MOBILE_DEVICE, + OPERATION_METHOD_REMOTE, +) +from .entity import AugustEntityMixin + +_LOGGER = logging.getLogger(__name__) + + +def _retrieve_device_battery_state(detail): + """Get the latest state of the sensor.""" + return detail.battery_level + + +def _retrieve_linked_keypad_battery_state(detail): + """Get the latest state of the sensor.""" + return detail.battery_percentage + + +SENSOR_TYPES_BATTERY = { + "device_battery": {"state_provider": _retrieve_device_battery_state}, + "linked_keypad_battery": {"state_provider": _retrieve_linked_keypad_battery_state}, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the August sensors.""" + data = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] + entities = [] + migrate_unique_id_devices = [] + operation_sensors = [] + batteries = { + "device_battery": [], + "linked_keypad_battery": [], + } + for device in data.doorbells: + batteries["device_battery"].append(device) + for device in data.locks: + batteries["device_battery"].append(device) + batteries["linked_keypad_battery"].append(device) + operation_sensors.append(device) + + for device in batteries["device_battery"]: + state_provider = SENSOR_TYPES_BATTERY["device_battery"]["state_provider"] + detail = data.get_device_detail(device.device_id) + if detail is None or state_provider(detail) is None: + _LOGGER.debug( + "Not adding battery sensor for %s because it is not present", + device.device_name, + ) + continue + _LOGGER.debug( + "Adding battery sensor for %s", + device.device_name, + ) + entities.append(AugustBatterySensor(data, "device_battery", device, device)) + + for device in batteries["linked_keypad_battery"]: + detail = data.get_device_detail(device.device_id) + + if detail.keypad is None: + _LOGGER.debug( + "Not adding keypad battery sensor for %s because it is not present", + device.device_name, + ) + continue + _LOGGER.debug( + "Adding keypad battery sensor for %s", + device.device_name, + ) + keypad_battery_sensor = AugustBatterySensor( + data, "linked_keypad_battery", detail.keypad, device + ) + entities.append(keypad_battery_sensor) + migrate_unique_id_devices.append(keypad_battery_sensor) + + for device in operation_sensors: + entities.append(AugustOperatorSensor(data, device)) + + await _async_migrate_old_unique_ids(hass, migrate_unique_id_devices) + + async_add_entities(entities) + + +async def _async_migrate_old_unique_ids(hass, devices): + """Keypads now have their own serial number.""" + registry = await async_get_registry(hass) + for device in devices: + old_entity_id = registry.async_get_entity_id( + "sensor", DOMAIN, device.old_unique_id + ) + if old_entity_id is not None: + _LOGGER.debug( + "Migrating unique_id from [%s] to [%s]", + device.old_unique_id, + device.unique_id, + ) + registry.async_update_entity(old_entity_id, new_unique_id=device.unique_id) + + +class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity): + """Representation of an August lock operation sensor.""" + + def __init__(self, data, device): + """Initialize the sensor.""" + super().__init__(data, device) + self._data = data + self._device = device + self._state = None + self._operated_remote = None + self._operated_keypad = None + self._operated_autorelock = None + self._operated_time = None + self._available = False + self._entity_picture = None + self._update_from_data() + + @property + def available(self): + """Return the availability of this sensor.""" + return self._available + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self._device.device_name} Operator" + + @callback + def _update_from_data(self): + """Get the latest state of the sensor and update activity.""" + lock_activity = self._data.activity_stream.get_latest_device_activity( + self._device_id, {ActivityType.LOCK_OPERATION} + ) + + self._available = True + if lock_activity is not None: + self._state = 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 + self._entity_picture = lock_activity.operator_thumbnail_url + + @property + def extra_state_attributes(self): + """Return the device specific state attributes.""" + attributes = {} + + if self._operated_remote is not None: + attributes[ATTR_OPERATION_REMOTE] = self._operated_remote + if self._operated_keypad is not None: + attributes[ATTR_OPERATION_KEYPAD] = self._operated_keypad + if self._operated_autorelock is not None: + attributes[ATTR_OPERATION_AUTORELOCK] = self._operated_autorelock + + if self._operated_remote: + attributes[ATTR_OPERATION_METHOD] = OPERATION_METHOD_REMOTE + elif self._operated_keypad: + attributes[ATTR_OPERATION_METHOD] = OPERATION_METHOD_KEYPAD + elif self._operated_autorelock: + attributes[ATTR_OPERATION_METHOD] = OPERATION_METHOD_AUTORELOCK + else: + attributes[ATTR_OPERATION_METHOD] = OPERATION_METHOD_MOBILE_DEVICE + + return attributes + + async def async_added_to_hass(self): + """Restore ATTR_CHANGED_BY on startup since it is likely no longer in the activity log.""" + await super().async_added_to_hass() + + last_state = await self.async_get_last_state() + if not last_state or last_state.state == STATE_UNAVAILABLE: + return + + self._state = last_state.state + if ATTR_ENTITY_PICTURE in last_state.attributes: + self._entity_picture = last_state.attributes[ATTR_ENTITY_PICTURE] + if ATTR_OPERATION_REMOTE in last_state.attributes: + self._operated_remote = last_state.attributes[ATTR_OPERATION_REMOTE] + if ATTR_OPERATION_KEYPAD in last_state.attributes: + self._operated_keypad = last_state.attributes[ATTR_OPERATION_KEYPAD] + if ATTR_OPERATION_AUTORELOCK in last_state.attributes: + self._operated_autorelock = last_state.attributes[ATTR_OPERATION_AUTORELOCK] + + @property + def entity_picture(self): + """Return the entity picture to use in the frontend, if any.""" + return self._entity_picture + + @property + def unique_id(self) -> str: + """Get the unique id of the device sensor.""" + return f"{self._device_id}_lock_operator" + + +class AugustBatterySensor(AugustEntityMixin, SensorEntity): + """Representation of an August sensor.""" + + def __init__(self, data, sensor_type, device, old_device): + """Initialize the sensor.""" + super().__init__(data, device) + self._data = data + self._sensor_type = sensor_type + self._device = device + self._old_device = old_device + self._state = None + self._available = False + self._update_from_data() + + @property + def available(self): + """Return the availability of this sensor.""" + return self._available + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return PERCENTAGE + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_BATTERY + + @property + def name(self): + """Return the name of the sensor.""" + device_name = self._device.device_name + return f"{device_name} Battery" + + @callback + def _update_from_data(self): + """Get the latest state of the sensor.""" + state_provider = SENSOR_TYPES_BATTERY[self._sensor_type]["state_provider"] + self._state = state_provider(self._detail) + self._available = self._state is not None + + @property + def unique_id(self) -> str: + """Get the unique id of the device sensor.""" + return f"{self._device_id}_{self._sensor_type}" + + @property + def old_unique_id(self) -> str: + """Get the old unique id of the device sensor.""" + return f"{self._old_device.device_id}_{self._sensor_type}" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/strings.json new file mode 100644 index 00000000000..7939fb1d25f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/strings.json @@ -0,0 +1,38 @@ +{ + "config": { + "error": { + "unknown": "[%key:common::config_flow::error::unknown%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + }, + "step": { + "validation": { + "title": "Two factor authentication", + "data": { + "code": "Verification code" + }, + "description": "Please check your {login_method} ({username}) and enter the verification code below" + }, + "user_validate": { + "description": "If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'.", + "data": { + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]", + "login_method": "Login Method" + }, + "title": "Setup an August account" + }, + "reauth_validate": { + "description": "Enter the password for {username}.", + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "title": "Reauthenticate an August account" + } + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/subscriber.py b/homeassistant-2021.6.0.dev0/homeassistant/components/august/subscriber.py new file mode 100644 index 00000000000..5223b8b4a38 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/subscriber.py @@ -0,0 +1,76 @@ +"""Base class for August entity.""" + + +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import callback +from homeassistant.helpers.event import async_track_time_interval + + +class AugustSubscriberMixin: + """Base implementation for a subscriber.""" + + def __init__(self, hass, update_interval): + """Initialize an subscriber.""" + super().__init__() + self._hass = hass + self._update_interval = update_interval + self._subscriptions = {} + self._unsub_interval = None + self._stop_interval = None + + @callback + def async_subscribe_device_id(self, device_id, update_callback): + """Add an callback subscriber. + + Returns a callable that can be used to unsubscribe. + """ + if not self._subscriptions: + self._async_setup_listeners() + + self._subscriptions.setdefault(device_id, []).append(update_callback) + + def _unsubscribe(): + self.async_unsubscribe_device_id(device_id, update_callback) + + return _unsubscribe + + @callback + def _async_setup_listeners(self): + """Create interval and stop listeners.""" + self._unsub_interval = async_track_time_interval( + self._hass, self._async_refresh, self._update_interval + ) + + @callback + def _async_cancel_update_interval(_): + self._stop_interval = None + self._unsub_interval() + + self._stop_interval = self._hass.bus.async_listen( + EVENT_HOMEASSISTANT_STOP, _async_cancel_update_interval + ) + + @callback + def async_unsubscribe_device_id(self, device_id, update_callback): + """Remove a callback subscriber.""" + self._subscriptions[device_id].remove(update_callback) + if not self._subscriptions[device_id]: + del self._subscriptions[device_id] + + if self._subscriptions: + return + + self._unsub_interval() + self._unsub_interval = None + if self._stop_interval: + self._stop_interval() + self._stop_interval = None + + @callback + def async_signal_device_id_update(self, device_id): + """Call the callbacks for a device_id.""" + if not self._subscriptions.get(device_id): + return + + for update_callback in self._subscriptions[device_id]: + update_callback() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/ca.json new file mode 100644 index 00000000000..f0b1fa43c3d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/ca.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja ha estat configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "reauth_validate": { + "data": { + "password": "Contrasenya" + }, + "description": "Introdueix la contrasenya per a {username}.", + "title": "Torna a autenticar compte d'August" + }, + "user_validate": { + "data": { + "login_method": "M\u00e8tode d'inici de sessi\u00f3", + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Si el m\u00e8tode d'inici de sessi\u00f3 \u00e9s 'email', el nom d'usuari \u00e9s l'adre\u00e7a de correu electr\u00f2nic. Si el m\u00e8tode d'inici de sessi\u00f3 \u00e9s 'phone', el nom d'usuari \u00e9s el n\u00famero de tel\u00e8fon en format \"+NNNNNNNNN\".", + "title": "Configuraci\u00f3 de compte August" + }, + "validation": { + "data": { + "code": "Codi de verificaci\u00f3" + }, + "description": "Comprova el teu {login_method} ({username}) i introdueix el codi de verificaci\u00f3 a continuaci\u00f3", + "title": "Verificaci\u00f3 en dos passos" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/cs.json new file mode 100644 index 00000000000..6cb0fbd238d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/cs.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_validate": { + "data": { + "password": "Heslo" + } + }, + "user_validate": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "validation": { + "data": { + "code": "Ov\u011b\u0159ovac\u00ed k\u00f3d" + }, + "description": "Zkontrolujte pros\u00edm {login_method} ({username}) a n\u00ed\u017ee zadejte ov\u011b\u0159ovac\u00ed k\u00f3d", + "title": "Dvoufaktorov\u00e9 ov\u011b\u0159ov\u00e1n\u00ed" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/da.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/da.json new file mode 100644 index 00000000000..de3fe4e8639 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/da.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigureret" + }, + "error": { + "cannot_connect": "Kunne ikke oprette forbindelse. Pr\u00f8v igen", + "invalid_auth": "Ugyldig godkendelse", + "unknown": "Uventet fejl" + }, + "step": { + "validation": { + "data": { + "code": "Bekr\u00e6ftelseskode" + }, + "description": "Kontroller dit {login_method} ({username}), og angiv bekr\u00e6ftelseskoden nedenfor", + "title": "Tofaktorgodkendelse" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/de.json new file mode 100644 index 00000000000..d2e08a5377c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/de.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "reauth_validate": { + "data": { + "password": "Passwort" + }, + "description": "Gib das Passwort f\u00fcr {username} ein.", + "title": "August-Konto erneut authentifizieren" + }, + "user_validate": { + "data": { + "login_method": "Anmeldemethode", + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Wenn die Anmeldemethode \"E-Mail\" lautet, ist Benutzername die E-Mail-Adresse. Wenn die Anmeldemethode \"Telefon\" ist, ist Benutzername die Telefonnummer im Format \"+ NNNNNNNNN\".", + "title": "Richte ein August-Konto ein" + }, + "validation": { + "data": { + "code": "Verifizierungs-Code" + }, + "description": "Bitte \u00fcberpr\u00fcfen Sie Ihre {login_method} ({username}) und geben Sie den Best\u00e4tigungscode ein", + "title": "Zwei-Faktor-Authentifizierung" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/el.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/el.json new file mode 100644 index 00000000000..8a976d451be --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/el.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "reauth_validate": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {username} .", + "title": "\u0395\u03c0\u03b9\u03ba\u03c5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03ad\u03bd\u03b1\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc August" + }, + "user_validate": { + "data": { + "login_method": "\u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2 \u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "description": "\u0395\u03ac\u03bd \u03b7 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \"\u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b5\u03af\u03bf\", \u03c4\u03bf \u038c\u03bd\u03bf\u03bc\u03b1 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b5\u03af\u03bf\u03c5. \u0395\u03ac\u03bd \u03b7 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \"\u03c4\u03b7\u03bb\u03ad\u03c6\u03c9\u03bd\u03bf\", \u03c4\u03bf \u038c\u03bd\u03bf\u03bc\u03b1 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bf \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c4\u03b7\u03bb\u03b5\u03c6\u03ce\u03bd\u03bf\u03c5 \u03bc\u03b5 \u03c4\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae '+NNNNNNNNNN'.", + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc August" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/en.json new file mode 100644 index 00000000000..f2ceef78d48 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/en.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "reauth_validate": { + "data": { + "password": "Password" + }, + "description": "Enter the password for {username}.", + "title": "Reauthenticate an August account" + }, + "user_validate": { + "data": { + "login_method": "Login Method", + "password": "Password", + "username": "Username" + }, + "description": "If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'.", + "title": "Setup an August account" + }, + "validation": { + "data": { + "code": "Verification code" + }, + "description": "Please check your {login_method} ({username}) and enter the verification code below", + "title": "Two factor authentication" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/es-419.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/es-419.json new file mode 100644 index 00000000000..7e5fe76d3af --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/es-419.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "validation": { + "data": { + "code": "C\u00f3digo de verificaci\u00f3n" + }, + "description": "Verifique su {login_method} ( {username} ) e ingrese el c\u00f3digo de verificaci\u00f3n a continuaci\u00f3n", + "title": "Autenticaci\u00f3n de dos factores" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/es.json new file mode 100644 index 00000000000..a660d11f996 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/es.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya ha sido configurada", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "reauth_validate": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "Introduzca la contrase\u00f1a de {username}.", + "title": "Reautorizar una cuenta de August" + }, + "user_validate": { + "data": { + "login_method": "M\u00e9todo de inicio de sesi\u00f3n", + "password": "Contrase\u00f1a", + "username": "Usuario" + }, + "description": "Si el m\u00e9todo de inicio de sesi\u00f3n es \"correo electr\u00f3nico\", el nombre de usuario es la direcci\u00f3n de correo electr\u00f3nico. Si el m\u00e9todo de inicio de sesi\u00f3n es \"tel\u00e9fono\", el nombre de usuario es el n\u00famero de tel\u00e9fono en el formato \"+NNNNNNN\".", + "title": "Configurar una cuenta de August" + }, + "validation": { + "data": { + "code": "C\u00f3digo de verificaci\u00f3n" + }, + "description": "Por favor, comprueba tu {login_method} ({username}) e introduce el c\u00f3digo de verificaci\u00f3n a continuaci\u00f3n", + "title": "Autenticaci\u00f3n de dos factores" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/et.json new file mode 100644 index 00000000000..e310df86374 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/et.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Tundmatu viga" + }, + "step": { + "reauth_validate": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Sisesta kasutaja {username} salas\u00f5na.", + "title": "Autendi Augusti konto uuesti" + }, + "user_validate": { + "data": { + "login_method": "Sisselogimismeetod", + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "Kui sisselogimismeetod on \"e-post\" on kasutajanimi e-posti aadress. Kui sisselogimismeetod on \"telefon\" on kasutajanimi telefoninumber vormingus \"+NNNNNNNNN\".", + "title": "Seadista Augusti sidumise konto" + }, + "validation": { + "data": { + "code": "Kinnituskood" + }, + "description": "Kontrolli oma {login_method} ( {username} ) ja sisesta kinnituskood", + "title": "Kaheastmelinetuvastuskood" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/fr.json new file mode 100644 index 00000000000..aebb72a76ed --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/fr.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "reauth_validate": { + "data": { + "password": "Mot de passe" + }, + "description": "Saisissez le mot de passe de {username} .", + "title": "R\u00e9authentifier un compte August" + }, + "user_validate": { + "data": { + "login_method": "M\u00e9thode de connexion", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "Si la m\u00e9thode de connexion est \u00abemail\u00bb, le nom d'utilisateur est l'adresse e-mail. Si la m\u00e9thode de connexion est \u00abt\u00e9l\u00e9phone\u00bb, le nom d'utilisateur est le num\u00e9ro de t\u00e9l\u00e9phone au format \u00ab+ NNNNNNNNN\u00bb.", + "title": "Cr\u00e9er un compte August" + }, + "validation": { + "data": { + "code": "Code de v\u00e9rification" + }, + "description": "Veuillez v\u00e9rifier votre {login_method} ( {username} ) et entrez le code de v\u00e9rification ci-dessous", + "title": "Authentification \u00e0 deux facteurs" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/hu.json new file mode 100644 index 00000000000..f95d180b4b5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/hu.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "reauth_validate": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "Add meg a(z) {username} jelszav\u00e1t.", + "title": "August fi\u00f3k \u00fajrahiteles\u00edt\u00e9se" + }, + "user_validate": { + "data": { + "login_method": "Bejelentkez\u00e9si m\u00f3d", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "title": "August fi\u00f3k be\u00e1ll\u00edt\u00e1sa" + }, + "validation": { + "data": { + "code": "Ellen\u0151rz\u0151 k\u00f3d" + }, + "title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/id.json new file mode 100644 index 00000000000..19c1309d8ed --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/id.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "reauth_validate": { + "data": { + "password": "Kata Sandi" + }, + "description": "Masukkan sandi untuk {username}.", + "title": "Autentikasi ulang akun August" + }, + "user_validate": { + "data": { + "login_method": "Metode Masuk", + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Jika Metode Masuk adalah 'email', Nama Pengguna adalah alamat email. Jika Metode Masuk adalah 'telepon', Nama Pengguna adalah nomor telepon dalam format '+NNNNNNNNN'.", + "title": "Siapkan akun August" + }, + "validation": { + "data": { + "code": "Kode verifikasi" + }, + "description": "Periksa {login_method} ({username}) Anda dan masukkan kode verifikasi di bawah ini", + "title": "Autentikasi dua faktor" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/it.json new file mode 100644 index 00000000000..0eb8b20f0a8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/it.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "reauth_validate": { + "data": { + "password": "Password" + }, + "description": "Inserisci la password per {username}.", + "title": "Riautentica un account di August" + }, + "user_validate": { + "data": { + "login_method": "Metodo di accesso", + "password": "Password", + "username": "Nome utente" + }, + "description": "Se il metodo di accesso \u00e8 'email', il nome utente \u00e8 l'indirizzo email. Se il metodo di accesso \u00e8 'phone', il nome utente \u00e8 il numero di telefono nel formato '+NNNNNNNNNN'.", + "title": "Configura un account di August" + }, + "validation": { + "data": { + "code": "Codice di verifica" + }, + "description": "Controlla il tuo {login_method} ({username}) e inserisci il codice di verifica seguente", + "title": "Autenticazione a due fattori" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/ko.json new file mode 100644 index 00000000000..1902d0112ff --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/ko.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "reauth_validate": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638" + }, + "description": "{username}\uc758 \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "August \uacc4\uc815 \uc7ac\uc778\uc99d\ud558\uae30" + }, + "user_validate": { + "data": { + "login_method": "\ub85c\uadf8\uc778 \ubc29\ubc95", + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "\ub85c\uadf8\uc778 \ubc29\ubc95\uc774 '\uc774\uba54\uc77c'\uc778 \uacbd\uc6b0, \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 \uc774\uba54\uc77c \uc8fc\uc18c\uc785\ub2c8\ub2e4. \ub85c\uadf8\uc778 \ubc29\ubc95\uc774 '\uc804\ud654\ubc88\ud638'\uc778 \uacbd\uc6b0, \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 '+NNNNNNNNN' \ud615\uc2dd\uc758 \uc804\ud654\ubc88\ud638\uc785\ub2c8\ub2e4.", + "title": "August \uacc4\uc815 \uc124\uc815\ud558\uae30" + }, + "validation": { + "data": { + "code": "\uc778\uc99d \ucf54\ub4dc" + }, + "description": "{login_method} ({username}) \uc744(\ub97c) \ud655\uc778\ud558\uace0 \uc544\ub798\uc5d0 \uc778\uc99d \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "2\ub2e8\uacc4 \uc778\uc99d" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/lb.json new file mode 100644 index 00000000000..16934964651 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/lb.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Kont ass scho konfigur\u00e9iert", + "reauth_successful": "Re-authentifikatioun war erfollegr\u00e4ich" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "reauth_validate": { + "description": "G\u00ebff Passwuert an fir {username}." + }, + "user_validate": { + "data": { + "login_method": "Login Method" + } + }, + "validation": { + "data": { + "code": "Verifikatiouns Code" + }, + "description": "Pr\u00e9ift w.e.g. \u00c4re {login_method} ({username}) a gitt de Verifikatiounscode hei dr\u00ebnner an", + "title": "2-Faktor-Authentifikatioun" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/nl.json new file mode 100644 index 00000000000..2d9a4202f74 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/nl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "reauth_validate": { + "data": { + "password": "Wachtwoord" + }, + "description": "Voer het wachtwoord in voor {username}.", + "title": "Verifieer opnieuw een August-account" + }, + "user_validate": { + "data": { + "login_method": "Inlogmethode", + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "Als de aanmeldingsmethode 'e-mail' is, is de gebruikersnaam het e-mailadres. Als de aanmeldingsmethode 'telefoon' is, is de gebruikersnaam het telefoonnummer in de indeling '+NNNNNNNNN'.", + "title": "Stel een August account in" + }, + "validation": { + "data": { + "code": "Verificatiecode" + }, + "description": "Controleer je {login_method} ( {username} ) en voer de onderstaande verificatiecode in", + "title": "Tweestapsverificatie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/no.json new file mode 100644 index 00000000000..8ea4cd7141f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/no.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen 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_validate": { + "data": { + "password": "Passord" + }, + "description": "Skriv inn passordet for {username} .", + "title": "Godkjenn en August-konto p\u00e5 nytt" + }, + "user_validate": { + "data": { + "login_method": "P\u00e5loggingsmetode", + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Hvis p\u00e5loggingsmetoden er 'e-post', er brukernavnet e-postadressen. Hvis p\u00e5loggingsmetoden er 'telefon', er brukernavn telefonnummeret i formatet '+ NNNNNNNNN'.", + "title": "Sett opp en August konto" + }, + "validation": { + "data": { + "code": "Bekreftelseskode" + }, + "description": "Kontroller {login_method} ({username}) og fyll inn bekreftelseskoden nedenfor", + "title": "Totrinnsbekreftelse" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/pl.json new file mode 100644 index 00000000000..d7deafd228d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/pl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "reauth_validate": { + "data": { + "password": "Has\u0142o" + }, + "description": "Wprowad\u017a has\u0142o dla {username}", + "title": "Ponownie uwierzytelnij konto August" + }, + "user_validate": { + "data": { + "login_method": "Metoda logowania", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Je\u015bli metod\u0105 logowania jest 'e-mail', nazw\u0105 u\u017cytkownika b\u0119dzie adres e-mail. Je\u015bli metod\u0105 logowania jest 'telefon', nazw\u0105 u\u017cytkownika b\u0119dzie numer telefonu w formacie '+NNNNNNNNN'.", + "title": "Konfiguracja konta August" + }, + "validation": { + "data": { + "code": "Kod weryfikacyjny" + }, + "description": "Sprawd\u017a {login_method} ({username}) i wprowad\u017a kod weryfikacyjny poni\u017cej", + "title": "Uwierzytelnianie dwusk\u0142adnikowe" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/pt-BR.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/pt-BR.json new file mode 100644 index 00000000000..7186be6216c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/pt-BR.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "validation": { + "data": { + "code": "C\u00f3digo de verifica\u00e7\u00e3o" + }, + "description": "Por favor, verifique o seu {login_method} ({username}) e digite o c\u00f3digo de verifica\u00e7\u00e3o abaixo", + "title": "Autentica\u00e7\u00e3o de dois fatores" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/pt.json new file mode 100644 index 00000000000..6c6765da70e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "validation": { + "data": { + "code": "C\u00f3digo de verifica\u00e7\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/ru.json new file mode 100644 index 00000000000..0f57924aef7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/ru.json @@ -0,0 +1,38 @@ +{ + "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_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": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "reauth_validate": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f" + }, + "user_validate": { + "data": { + "login_method": "\u0421\u043f\u043e\u0441\u043e\u0431 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u0415\u0441\u043b\u0438 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0441\u043f\u043e\u0441\u043e\u0431\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043e 'email', \u0442\u043e \u043b\u043e\u0433\u0438\u043d\u043e\u043c \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b. \u0415\u0441\u043b\u0438 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0441\u043f\u043e\u0441\u043e\u0431\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043e 'phone', \u0442\u043e \u043b\u043e\u0433\u0438\u043d\u043e\u043c \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0430 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 '+NNNNNNNNN'.", + "title": "August" + }, + "validation": { + "data": { + "code": "\u041a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f" + }, + "description": "\u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 {login_method} ({username}) \u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0439 \u043f\u0440\u043e\u0432\u0435\u0440\u043e\u0447\u043d\u044b\u0439 \u043a\u043e\u0434.", + "title": "\u0414\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/sl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/sl.json new file mode 100644 index 00000000000..41be45271c8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/sl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ra\u010dun je \u017ee nastavljen" + }, + "error": { + "cannot_connect": "Povezava ni uspela, poskusite znova", + "invalid_auth": "Neveljavna avtentikacija", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "validation": { + "data": { + "code": "Koda za preverjanje" + }, + "description": "Preverite svoj {login_method} ({username}) in spodaj vnesite verifikacijsko kodo", + "title": "Dvofaktorska avtentikacija" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/sv.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/sv.json new file mode 100644 index 00000000000..762d5fd7640 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/sv.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Kontot har redan konfigurerats" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "reauth_validate": { + "data": { + "password": "L\u00f6senord" + } + }, + "user_validate": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + }, + "validation": { + "data": { + "code": "Verifieringskod" + }, + "title": "Tv\u00e5faktorsautentisering" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/tr.json new file mode 100644 index 00000000000..d3b32080466 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "validation": { + "data": { + "code": "Do\u011frulama kodu" + }, + "description": "L\u00fctfen {login_method} ( {username} ) bilgilerinizi kontrol edin ve a\u015fa\u011f\u0131ya do\u011frulama kodunu girin", + "title": "\u0130ki fakt\u00f6rl\u00fc kimlik do\u011frulama" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/uk.json new file mode 100644 index 00000000000..5f4729d02b2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "validation": { + "data": { + "code": "\u041a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f" + }, + "description": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 {login_method} ({username}) \u0456 \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u0438\u0439 \u043a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f.", + "title": "\u0414\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/zh-Hant.json new file mode 100644 index 00000000000..17fd85a2d58 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/august/translations/zh-Hant.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "reauth_validate": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "\u8f38\u5165{username} \u5bc6\u78bc", + "title": "\u91cd\u65b0\u8a8d\u8b49 August \u5e33\u865f" + }, + "user_validate": { + "data": { + "login_method": "\u767b\u5165\u65b9\u5f0f", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u5047\u5982\u767b\u5165\u65b9\u5f0f\u70ba\u90f5\u4ef6\u300cemail\u300d\u3001\u4f7f\u7528\u8005\u540d\u7a31\u70ba\u96fb\u5b50\u90f5\u4ef6\u4f4d\u5740\u3002\u5047\u5982\u767b\u5165\u65b9\u5f0f\u70ba\u96fb\u8a71\u300cphone\u300d\u3001\u5247\u4f7f\u7528\u8005\u540d\u7a31\u70ba\u5305\u542b\u570b\u78bc\u4e4b\u96fb\u8a71\u865f\u78bc\uff0c\u5982\u300c+NNNNNNNNN\u300d\u3002", + "title": "\u8a2d\u5b9a August \u5e33\u865f" + }, + "validation": { + "data": { + "code": "\u9a57\u8b49\u78bc" + }, + "description": "\u8acb\u78ba\u8a8d {login_method} ({username}) \u4e26\u65bc\u4e0b\u65b9\u8f38\u5165\u9a57\u8b49\u78bc", + "title": "\u96d9\u91cd\u8a8d\u8b49" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/__init__.py new file mode 100644 index 00000000000..e565071eae2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/__init__.py @@ -0,0 +1,169 @@ +"""The aurora component.""" + +from datetime import timedelta +import logging + +from aiohttp import ClientError +from auroranoaa import AuroraForecast + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import ( + ATTR_ENTRY_TYPE, + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTRIBUTION, + AURORA_API, + CONF_THRESHOLD, + COORDINATOR, + DEFAULT_POLLING_INTERVAL, + DEFAULT_THRESHOLD, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["binary_sensor", "sensor"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Aurora from a config entry.""" + + conf = entry.data + options = entry.options + + session = aiohttp_client.async_get_clientsession(hass) + api = AuroraForecast(session) + + longitude = conf[CONF_LONGITUDE] + latitude = conf[CONF_LATITUDE] + polling_interval = DEFAULT_POLLING_INTERVAL + threshold = options.get(CONF_THRESHOLD, DEFAULT_THRESHOLD) + name = conf[CONF_NAME] + + coordinator = AuroraDataUpdateCoordinator( + hass=hass, + name=name, + polling_interval=polling_interval, + api=api, + latitude=latitude, + longitude=longitude, + threshold=threshold, + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + COORDINATOR: coordinator, + AURORA_API: api, + } + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """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 AuroraDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching data from the NOAA Aurora API.""" + + def __init__( + self, + hass: HomeAssistant, + name: str, + polling_interval: int, + api: str, + latitude: float, + longitude: float, + threshold: float, + ): + """Initialize the data updater.""" + + super().__init__( + hass=hass, + logger=_LOGGER, + name=name, + update_interval=timedelta(minutes=polling_interval), + ) + + self.api = api + self.name = name + self.latitude = int(latitude) + self.longitude = int(longitude) + self.threshold = int(threshold) + + async def _async_update_data(self): + """Fetch the data from the NOAA Aurora Forecast.""" + + try: + return await self.api.get_forecast_data(self.longitude, self.latitude) + except ClientError as error: + raise UpdateFailed(f"Error updating from NOAA: {error}") from error + + +class AuroraEntity(CoordinatorEntity): + """Implementation of the base Aurora Entity.""" + + def __init__( + self, + coordinator: AuroraDataUpdateCoordinator, + name: str, + icon: str, + ): + """Initialize the Aurora Entity.""" + + super().__init__(coordinator=coordinator) + + self._name = name + self._unique_id = f"{self.coordinator.latitude}_{self.coordinator.longitude}" + self._icon = icon + + @property + def unique_id(self): + """Define the unique id based on the latitude and longitude.""" + return self._unique_id + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return {"attribution": ATTRIBUTION} + + @property + def icon(self): + """Return the icon for the sensor.""" + return self._icon + + @property + def device_info(self): + """Define the device based on name.""" + return { + ATTR_IDENTIFIERS: {(DOMAIN, self._unique_id)}, + ATTR_NAME: self.coordinator.name, + ATTR_MANUFACTURER: "NOAA", + ATTR_MODEL: "Aurora Visibility Sensor", + ATTR_ENTRY_TYPE: "service", + } diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/binary_sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/binary_sensor.py new file mode 100644 index 00000000000..a6d5a1817b2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/binary_sensor.py @@ -0,0 +1,24 @@ +"""Support for Aurora Forecast binary sensor.""" +from homeassistant.components.binary_sensor import BinarySensorEntity + +from . import AuroraEntity +from .const import COORDINATOR, DOMAIN + + +async def async_setup_entry(hass, entry, async_add_entries): + """Set up the binary_sensor platform.""" + coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] + name = f"{coordinator.name} Aurora Visibility Alert" + + entity = AuroraSensor(coordinator=coordinator, name=name, icon="mdi:hazard-lights") + + async_add_entries([entity]) + + +class AuroraSensor(AuroraEntity, BinarySensorEntity): + """Implementation of an aurora sensor.""" + + @property + def is_on(self): + """Return true if aurora is visible.""" + return self.coordinator.data > self.coordinator.threshold diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/config_flow.py new file mode 100644 index 00000000000..fd9ebbf424c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/config_flow.py @@ -0,0 +1,110 @@ +"""Config flow for SpaceX Launches and Starman.""" +import logging + +from aiohttp import ClientError +from auroranoaa import AuroraForecast +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client + +from .const import CONF_THRESHOLD, DEFAULT_NAME, DEFAULT_THRESHOLD, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for NOAA Aurora Integration.""" + + VERSION = 1 + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + + if user_input is not None: + name = user_input[CONF_NAME] + longitude = user_input[CONF_LONGITUDE] + latitude = user_input[CONF_LATITUDE] + + session = aiohttp_client.async_get_clientsession(self.hass) + api = AuroraForecast(session=session) + + try: + await api.get_forecast_data(longitude, latitude) + except ClientError: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id( + f"{user_input[CONF_LONGITUDE]}_{user_input[CONF_LATITUDE]}" + ) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=f"Aurora - {name}", data=user_input + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_NAME, default=DEFAULT_NAME): str, + vol.Required( + CONF_LONGITUDE, + default=self.hass.config.longitude, + ): vol.All( + vol.Coerce(float), + vol.Range(min=-180, max=180), + ), + vol.Required( + CONF_LATITUDE, + default=self.hass.config.latitude, + ): vol.All( + vol.Coerce(float), + vol.Range(min=-90, max=90), + ), + } + ), + errors=errors, + ) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle options flow changes.""" + + def __init__(self, config_entry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage options.""" + + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required( + CONF_THRESHOLD, + default=self.config_entry.options.get( + CONF_THRESHOLD, DEFAULT_THRESHOLD + ), + ): vol.All( + vol.Coerce(int), + vol.Range(min=0, max=100), + ), + } + ), + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/const.py new file mode 100644 index 00000000000..cd6f54a3d0c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/const.py @@ -0,0 +1,14 @@ +"""Constants for the Aurora integration.""" + +DOMAIN = "aurora" +COORDINATOR = "coordinator" +AURORA_API = "aurora_api" +ATTR_IDENTIFIERS = "identifiers" +ATTR_MANUFACTURER = "manufacturer" +ATTR_MODEL = "model" +ATTR_ENTRY_TYPE = "entry_type" +DEFAULT_POLLING_INTERVAL = 5 +CONF_THRESHOLD = "forecast_threshold" +DEFAULT_THRESHOLD = 75 +ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric Administration" +DEFAULT_NAME = "Aurora Visibility" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/manifest.json new file mode 100644 index 00000000000..466bf938cb5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "aurora", + "name": "Aurora", + "documentation": "https://www.home-assistant.io/integrations/aurora", + "config_flow": true, + "codeowners": ["@djtimca"], + "requirements": ["auroranoaa==0.0.2"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/sensor.py new file mode 100644 index 00000000000..d7024cc630a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/sensor.py @@ -0,0 +1,33 @@ +"""Support for Aurora Forecast sensor.""" +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import PERCENTAGE + +from . import AuroraEntity +from .const import COORDINATOR, DOMAIN + + +async def async_setup_entry(hass, entry, async_add_entries): + """Set up the sensor platform.""" + coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] + + entity = AuroraSensor( + coordinator=coordinator, + name=f"{coordinator.name} Aurora Visibility %", + icon="mdi:gauge", + ) + + async_add_entries([entity]) + + +class AuroraSensor(AuroraEntity, SensorEntity): + """Implementation of an aurora sensor.""" + + @property + def state(self): + """Return % chance the aurora is visible.""" + return self.coordinator.data + + @property + def unit_of_measurement(self): + """Return the unit of measure.""" + return PERCENTAGE diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/strings.json new file mode 100644 index 00000000000..31af19748d6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/strings.json @@ -0,0 +1,26 @@ +{ + "title": "NOAA Aurora Sensor", + "config": { + "step": { + "user": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "longitude": "[%key:common::config_flow::data::longitude%]", + "latitude": "[%key:common::config_flow::data::latitude%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Threshold (%)" + } + } + } + } + } \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/ca.json new file mode 100644 index 00000000000..99db9855e74 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/ca.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nom" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Llindar (%)" + } + } + } + }, + "title": "Sensor Aurora NOAA" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/cs.json new file mode 100644 index 00000000000..e7a10c94241 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/cs.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", + "name": "Jm\u00e9no" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Pr\u00e1h (%)" + } + } + } + }, + "title": "Senzor NOAA Aurora" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/de.json new file mode 100644 index 00000000000..838673e8d60 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/de.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "name": "Name" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Schwellenwert (%)" + } + } + } + }, + "title": "NOAA Aurora-Sensor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/en.json new file mode 100644 index 00000000000..e3e36574608 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Threshold (%)" + } + } + } + }, + "title": "NOAA Aurora Sensor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/es.json new file mode 100644 index 00000000000..c722c95ef6f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Umbral (%)" + } + } + } + }, + "title": "Sensor Aurora NOAA" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/et.json new file mode 100644 index 00000000000..80fb6b21736 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/et.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendus nurjus" + }, + "step": { + "user": { + "data": { + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad", + "name": "Nimi" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "L\u00e4vi (%)" + } + } + } + }, + "title": "NOAA Aurora andur" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/fr.json new file mode 100644 index 00000000000..473ecefdbd9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/fr.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00c9chec \u00e0 la connexion" + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nom" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Seuil (%)" + } + } + } + }, + "title": "Capteur NOAA Aurora" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/hu.json new file mode 100644 index 00000000000..292ed552235 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/hu.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "name": "N\u00e9v" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "K\u00fcsz\u00f6b (%)" + } + } + } + }, + "title": "Nemzeti \u00d3ce\u00e1n- \u00e9s L\u00e9gk\u00f6rkutat\u00e1si Hivatal (NOAA) Aurora \u00e9rz\u00e9kel\u0151" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/id.json new file mode 100644 index 00000000000..66cf534b7ae --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/id.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "latitude": "Lintang", + "longitude": "Bujur", + "name": "Nama" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Ambang (%)" + } + } + } + }, + "title": "Sensor Aurora NOAA" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/it.json new file mode 100644 index 00000000000..4350fbf555a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "latitude": "Latitudine", + "longitude": "Logitudine", + "name": "Nome" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Soglia (%)" + } + } + } + }, + "title": "Sensore NOAA Aurora" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/ka.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/ka.json new file mode 100644 index 00000000000..f677f54e32e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/ka.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d8 \u10d5\u10d4\u10e0 \u10d3\u10d0\u10db\u10e7\u10d0\u10e0\u10d3\u10d0" + }, + "step": { + "user": { + "data": { + "latitude": "\u10d2\u10d0\u10dc\u10d4\u10d3\u10d8", + "longitude": "\u10d2\u10e0\u10eb\u10d4\u10d3\u10d8", + "name": "\u10e1\u10d0\u10ee\u10d4\u10da\u10d8" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "\u10d6\u10e6\u10d5\u10d0\u10e0\u10d8 (%)" + } + } + } + }, + "title": "NOAA Aurora \u10e1\u10d4\u10dc\u10e1\u10dd\u10e0\u10d8" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/ko.json new file mode 100644 index 00000000000..39f3c4d4281 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/ko.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "name": "\uc774\ub984" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "\uc784\uacc4\uac12 (%)" + } + } + } + }, + "title": "NOAA Aurora \uc13c\uc11c" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/lb.json new file mode 100644 index 00000000000..e03a50e0183 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/lb.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "Feeler beim verbannen" + }, + "step": { + "user": { + "data": { + "latitude": "L\u00e4ngregraad", + "longitude": "Breedegrad", + "name": "Numm" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Grenzw\u00e4ert (%)" + } + } + } + }, + "title": "NOAA Aurora Sensor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/nl.json new file mode 100644 index 00000000000..fe7b4809f13 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/nl.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "name": "Naam" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Drempel (%)" + } + } + } + }, + "title": "NOAA Aurora Sensor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/no.json new file mode 100644 index 00000000000..1d22d6cd08b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "name": "Navn" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Terskel (%)" + } + } + } + }, + "title": "NOAA Aurora-sensor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/pl.json new file mode 100644 index 00000000000..f8786290458 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/pl.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Pr\u00f3g prawdopodobie\u0144stwa (%)" + } + } + } + }, + "title": "Sensor NOAA Aurora" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/pt.json new file mode 100644 index 00000000000..336f6ac5f68 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/ru.json new file mode 100644 index 00000000000..20e8f4a184b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/ru.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "\u041f\u043e\u0440\u043e\u0433 (%)" + } + } + } + }, + "title": "NOAA Aurora Sensor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/sl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/sl.json new file mode 100644 index 00000000000..d4e640e4069 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/sl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Ime" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/tr.json new file mode 100644 index 00000000000..0c3bb75ed6e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "latitude": "Enlem", + "longitude": "Boylam", + "name": "Ad" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/uk.json new file mode 100644 index 00000000000..0cb3c4fcbce --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/uk.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "\u041f\u043e\u0440\u0456\u0433 (%)" + } + } + } + }, + "title": "NOAA Aurora Sensor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/zh-Hans.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/zh-Hans.json new file mode 100644 index 00000000000..e28e3121f38 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u540d\u79f0" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/zh-Hant.json new file mode 100644 index 00000000000..e1824a7ff4a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora/translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u540d\u7a31" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "\u95a5\u503c (%)" + } + } + } + }, + "title": "NOAA Aurora \u50b3\u611f\u5668" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora_abb_powerone/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora_abb_powerone/__init__.py new file mode 100644 index 00000000000..087172d1bb5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora_abb_powerone/__init__.py @@ -0,0 +1 @@ +"""The Aurora ABB Powerone PV inverter sensor integration.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora_abb_powerone/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora_abb_powerone/manifest.json new file mode 100644 index 00000000000..69798ce4906 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora_abb_powerone/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "aurora_abb_powerone", + "name": "Aurora ABB Solar PV", + "documentation": "https://www.home-assistant.io/integrations/aurora_abb_powerone/", + "codeowners": ["@davet2001"], + "requirements": ["aurorapy==0.2.6"], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora_abb_powerone/sensor.py new file mode 100644 index 00000000000..f4640e7c014 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aurora_abb_powerone/sensor.py @@ -0,0 +1,102 @@ +"""Support for Aurora ABB PowerOne Solar Photvoltaic (PV) inverter.""" + +import logging + +from aurorapy.client import AuroraError, AuroraSerialClient +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.const import ( + CONF_ADDRESS, + CONF_DEVICE, + CONF_NAME, + DEVICE_CLASS_POWER, + POWER_WATT, +) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_ADDRESS = 2 +DEFAULT_NAME = "Solar PV" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_DEVICE): cv.string, + vol.Optional(CONF_ADDRESS, default=DEFAULT_ADDRESS): cv.positive_int, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Aurora ABB PowerOne device.""" + devices = [] + comport = config[CONF_DEVICE] + address = config[CONF_ADDRESS] + name = config[CONF_NAME] + + _LOGGER.debug("Intitialising com port=%s address=%s", comport, address) + client = AuroraSerialClient(address, comport, parity="N", timeout=1) + + devices.append(AuroraABBSolarPVMonitorSensor(client, name, "Power")) + add_entities(devices, True) + + +class AuroraABBSolarPVMonitorSensor(SensorEntity): + """Representation of a Sensor.""" + + def __init__(self, client, name, typename): + """Initialize the sensor.""" + self._name = f"{name} {typename}" + self.client = client + self._state = None + + @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._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return POWER_WATT + + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_POWER + + def update(self): + """Fetch new state data for the sensor. + + This is the only method that should fetch new data for Home Assistant. + """ + try: + self.client.connect() + # read ADC channel 3 (grid power output) + power_watts = self.client.measure(3, True) + self._state = round(power_watts, 1) + # _LOGGER.debug("Got reading %fW" % self._state) + except AuroraError as error: + # aurorapy does not have different exceptions (yet) for dealing + # with timeout vs other comms errors. + # This means the (normal) situation of no response during darkness + # raises an exception. + # aurorapy (gitlab) pull request merged 29/5/2019. When >0.2.6 is + # released, this could be modified to : + # except AuroraTimeoutError as e: + # Workaround: look at the text of the exception + if "No response after" in str(error): + _LOGGER.debug("No response from inverter (could be dark)") + else: + raise error + self._state = None + finally: + if self.client.serline.isOpen(): + self.client.close() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/__init__.py new file mode 100644 index 00000000000..7381be5e9de --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/__init__.py @@ -0,0 +1,581 @@ +"""Component to allow users to login and get tokens. + +# POST /auth/token + +This is an OAuth2 endpoint for granting tokens. We currently support the grant +types "authorization_code" and "refresh_token". Because we follow the OAuth2 +spec, data should be send in formatted as x-www-form-urlencoded. Examples will +be in JSON as it's more readable. + +## Grant type authorization_code + +Exchange the authorization code retrieved from the login flow for tokens. + +{ + "client_id": "https://hassbian.local:8123/", + "grant_type": "authorization_code", + "code": "411ee2f916e648d691e937ae9344681e" +} + +Return value will be the access and refresh tokens. The access token will have +a limited expiration. New access tokens can be requested using the refresh +token. + +{ + "access_token": "ABCDEFGH", + "expires_in": 1800, + "refresh_token": "IJKLMNOPQRST", + "token_type": "Bearer" +} + +## Grant type refresh_token + +Request a new access token using a refresh token. + +{ + "client_id": "https://hassbian.local:8123/", + "grant_type": "refresh_token", + "refresh_token": "IJKLMNOPQRST" +} + +Return value will be a new access token. The access token will have +a limited expiration. + +{ + "access_token": "ABCDEFGH", + "expires_in": 1800, + "token_type": "Bearer" +} + +## Revoking a refresh token + +It is also possible to revoke a refresh token and all access tokens that have +ever been granted by that refresh token. Response code will ALWAYS be 200. + +{ + "token": "IJKLMNOPQRST", + "action": "revoke" +} + +# Websocket API + +## Get current user + +Send websocket command `auth/current_user` will return current user of the +active websocket connection. + +{ + "id": 10, + "type": "auth/current_user", +} + +The result payload likes + +{ + "id": 10, + "type": "result", + "success": true, + "result": { + "id": "USER_ID", + "name": "John Doe", + "is_owner": true, + "credentials": [{ + "auth_provider_type": "homeassistant", + "auth_provider_id": null + }], + "mfa_modules": [{ + "id": "totp", + "name": "TOTP", + "enabled": true + }] + } +} + +## Create a long-lived access token + +Send websocket command `auth/long_lived_access_token` will create +a long-lived access token for current user. Access token will not be saved in +Home Assistant. User need to record the token in secure place. + +{ + "id": 11, + "type": "auth/long_lived_access_token", + "client_name": "GPS Logger", + "lifespan": 365 +} + +Result will be a long-lived access token: + +{ + "id": 11, + "type": "result", + "success": true, + "result": "ABCDEFGH" +} + +""" +from __future__ import annotations + +from datetime import timedelta +import uuid + +from aiohttp import web +import voluptuous as vol + +from homeassistant.auth import InvalidAuthError +from homeassistant.auth.models import ( + TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, + Credentials, + User, +) +from homeassistant.components import websocket_api +from homeassistant.components.http.auth import async_sign_path +from homeassistant.components.http.ban import log_invalid_auth +from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.components.http.view import HomeAssistantView +from homeassistant.const import HTTP_BAD_REQUEST, HTTP_FORBIDDEN, HTTP_OK +from homeassistant.core import HomeAssistant, callback +from homeassistant.loader import bind_hass +from homeassistant.util import dt as dt_util + +from . import indieauth, login_flow, mfa_setup_flow + +DOMAIN = "auth" +WS_TYPE_CURRENT_USER = "auth/current_user" +SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_CURRENT_USER} +) + +WS_TYPE_LONG_LIVED_ACCESS_TOKEN = "auth/long_lived_access_token" +SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + { + vol.Required("type"): WS_TYPE_LONG_LIVED_ACCESS_TOKEN, + vol.Required("lifespan"): int, # days + vol.Required("client_name"): str, + vol.Optional("client_icon"): str, + } +) + +WS_TYPE_REFRESH_TOKENS = "auth/refresh_tokens" +SCHEMA_WS_REFRESH_TOKENS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_REFRESH_TOKENS} +) + +WS_TYPE_DELETE_REFRESH_TOKEN = "auth/delete_refresh_token" +SCHEMA_WS_DELETE_REFRESH_TOKEN = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + { + vol.Required("type"): WS_TYPE_DELETE_REFRESH_TOKEN, + vol.Required("refresh_token_id"): str, + } +) + +WS_TYPE_SIGN_PATH = "auth/sign_path" +SCHEMA_WS_SIGN_PATH = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + { + vol.Required("type"): WS_TYPE_SIGN_PATH, + vol.Required("path"): str, + vol.Optional("expires", default=30): int, + } +) + +RESULT_TYPE_CREDENTIALS = "credentials" +RESULT_TYPE_USER = "user" + + +@bind_hass +def create_auth_code( + hass, client_id: str, credential_or_user: Credentials | User +) -> str: + """Create an authorization code to fetch tokens.""" + return hass.data[DOMAIN](client_id, credential_or_user) + + +async def async_setup(hass, config): + """Component to allow users to login.""" + store_result, retrieve_result = _create_auth_code_store() + + hass.data[DOMAIN] = store_result + + hass.http.register_view(TokenView(retrieve_result)) + hass.http.register_view(LinkUserView(retrieve_result)) + + hass.components.websocket_api.async_register_command( + WS_TYPE_CURRENT_USER, websocket_current_user, SCHEMA_WS_CURRENT_USER + ) + hass.components.websocket_api.async_register_command( + WS_TYPE_LONG_LIVED_ACCESS_TOKEN, + websocket_create_long_lived_access_token, + SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN, + ) + hass.components.websocket_api.async_register_command( + WS_TYPE_REFRESH_TOKENS, websocket_refresh_tokens, SCHEMA_WS_REFRESH_TOKENS + ) + hass.components.websocket_api.async_register_command( + WS_TYPE_DELETE_REFRESH_TOKEN, + websocket_delete_refresh_token, + SCHEMA_WS_DELETE_REFRESH_TOKEN, + ) + hass.components.websocket_api.async_register_command( + WS_TYPE_SIGN_PATH, websocket_sign_path, SCHEMA_WS_SIGN_PATH + ) + + await login_flow.async_setup(hass, store_result) + await mfa_setup_flow.async_setup(hass) + + return True + + +class TokenView(HomeAssistantView): + """View to issue or revoke tokens.""" + + url = "/auth/token" + name = "api:auth:token" + requires_auth = False + cors_allowed = True + + def __init__(self, retrieve_auth): + """Initialize the token view.""" + self._retrieve_auth = retrieve_auth + + @log_invalid_auth + async def post(self, request): + """Grant a token.""" + hass = request.app["hass"] + data = await request.post() + + grant_type = data.get("grant_type") + + # IndieAuth 6.3.5 + # The revocation endpoint is the same as the token endpoint. + # The revocation request includes an additional parameter, + # action=revoke. + if data.get("action") == "revoke": + return await self._async_handle_revoke_token(hass, data) + + if grant_type == "authorization_code": + return await self._async_handle_auth_code(hass, data, request.remote) + + if grant_type == "refresh_token": + return await self._async_handle_refresh_token(hass, data, request.remote) + + return self.json( + {"error": "unsupported_grant_type"}, status_code=HTTP_BAD_REQUEST + ) + + async def _async_handle_revoke_token(self, hass, data): + """Handle revoke token request.""" + # OAuth 2.0 Token Revocation [RFC7009] + # 2.2 The authorization server responds with HTTP status code 200 + # if the token has been revoked successfully or if the client + # submitted an invalid token. + token = data.get("token") + + if token is None: + return web.Response(status=HTTP_OK) + + refresh_token = await hass.auth.async_get_refresh_token_by_token(token) + + if refresh_token is None: + return web.Response(status=HTTP_OK) + + await hass.auth.async_remove_refresh_token(refresh_token) + return web.Response(status=HTTP_OK) + + async def _async_handle_auth_code(self, hass, data, remote_addr): + """Handle authorization code request.""" + client_id = data.get("client_id") + if client_id is None or not indieauth.verify_client_id(client_id): + return self.json( + {"error": "invalid_request", "error_description": "Invalid client id"}, + status_code=HTTP_BAD_REQUEST, + ) + + code = data.get("code") + + if code is None: + return self.json( + {"error": "invalid_request", "error_description": "Invalid code"}, + status_code=HTTP_BAD_REQUEST, + ) + + credential = self._retrieve_auth(client_id, RESULT_TYPE_CREDENTIALS, code) + + if credential is None or not isinstance(credential, Credentials): + return self.json( + {"error": "invalid_request", "error_description": "Invalid code"}, + status_code=HTTP_BAD_REQUEST, + ) + + user = await hass.auth.async_get_or_create_user(credential) + + if not user.is_active: + return self.json( + {"error": "access_denied", "error_description": "User is not active"}, + status_code=HTTP_FORBIDDEN, + ) + + refresh_token = await hass.auth.async_create_refresh_token( + user, client_id, credential=credential + ) + try: + access_token = hass.auth.async_create_access_token( + refresh_token, remote_addr + ) + except InvalidAuthError as exc: + return self.json( + {"error": "access_denied", "error_description": str(exc)}, + status_code=HTTP_FORBIDDEN, + ) + + return self.json( + { + "access_token": access_token, + "token_type": "Bearer", + "refresh_token": refresh_token.token, + "expires_in": int( + refresh_token.access_token_expiration.total_seconds() + ), + } + ) + + async def _async_handle_refresh_token(self, hass, data, remote_addr): + """Handle authorization code request.""" + client_id = data.get("client_id") + if client_id is not None and not indieauth.verify_client_id(client_id): + return self.json( + {"error": "invalid_request", "error_description": "Invalid client id"}, + status_code=HTTP_BAD_REQUEST, + ) + + token = data.get("refresh_token") + + if token is None: + return self.json({"error": "invalid_request"}, status_code=HTTP_BAD_REQUEST) + + refresh_token = await hass.auth.async_get_refresh_token_by_token(token) + + if refresh_token is None: + return self.json({"error": "invalid_grant"}, status_code=HTTP_BAD_REQUEST) + + if refresh_token.client_id != client_id: + return self.json({"error": "invalid_request"}, status_code=HTTP_BAD_REQUEST) + + try: + access_token = hass.auth.async_create_access_token( + refresh_token, remote_addr + ) + except InvalidAuthError as exc: + return self.json( + {"error": "access_denied", "error_description": str(exc)}, + status_code=HTTP_FORBIDDEN, + ) + + return self.json( + { + "access_token": access_token, + "token_type": "Bearer", + "expires_in": int( + refresh_token.access_token_expiration.total_seconds() + ), + } + ) + + +class LinkUserView(HomeAssistantView): + """View to link existing users to new credentials.""" + + url = "/auth/link_user" + name = "api:auth:link_user" + + def __init__(self, retrieve_credentials): + """Initialize the link user view.""" + self._retrieve_credentials = retrieve_credentials + + @RequestDataValidator(vol.Schema({"code": str, "client_id": str})) + async def post(self, request, data): + """Link a user.""" + hass = request.app["hass"] + user = request["hass_user"] + + credentials = self._retrieve_credentials( + data["client_id"], RESULT_TYPE_CREDENTIALS, data["code"] + ) + + if credentials is None: + return self.json_message("Invalid code", status_code=HTTP_BAD_REQUEST) + + await hass.auth.async_link_user(user, credentials) + return self.json_message("User linked") + + +@callback +def _create_auth_code_store(): + """Create an in memory store.""" + temp_results = {} + + @callback + def store_result(client_id, result): + """Store flow result and return a code to retrieve it.""" + if isinstance(result, User): + result_type = RESULT_TYPE_USER + elif isinstance(result, Credentials): + result_type = RESULT_TYPE_CREDENTIALS + else: + raise ValueError("result has to be either User or Credentials") + + code = uuid.uuid4().hex + temp_results[(client_id, result_type, code)] = ( + dt_util.utcnow(), + result_type, + result, + ) + return code + + @callback + def retrieve_result(client_id, result_type, code): + """Retrieve flow result.""" + key = (client_id, result_type, code) + + if key not in temp_results: + return None + + created, _, result = temp_results.pop(key) + + # OAuth 4.2.1 + # The authorization code MUST expire shortly after it is issued to + # mitigate the risk of leaks. A maximum authorization code lifetime of + # 10 minutes is RECOMMENDED. + if dt_util.utcnow() - created < timedelta(minutes=10): + return result + + return None + + return store_result, retrieve_result + + +@websocket_api.ws_require_user() +@websocket_api.async_response +async def websocket_current_user( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg +): + """Return the current user.""" + user = connection.user + enabled_modules = await hass.auth.async_get_enabled_mfa(user) + + connection.send_message( + websocket_api.result_message( + msg["id"], + { + "id": user.id, + "name": user.name, + "is_owner": user.is_owner, + "is_admin": user.is_admin, + "credentials": [ + { + "auth_provider_type": c.auth_provider_type, + "auth_provider_id": c.auth_provider_id, + } + for c in user.credentials + ], + "mfa_modules": [ + { + "id": module.id, + "name": module.name, + "enabled": module.id in enabled_modules, + } + for module in hass.auth.auth_mfa_modules + ], + }, + ) + ) + + +@websocket_api.ws_require_user() +@websocket_api.async_response +async def websocket_create_long_lived_access_token( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg +): + """Create or a long-lived access token.""" + refresh_token = await hass.auth.async_create_refresh_token( + connection.user, + client_name=msg["client_name"], + client_icon=msg.get("client_icon"), + token_type=TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, + access_token_expiration=timedelta(days=msg["lifespan"]), + ) + + try: + access_token = hass.auth.async_create_access_token(refresh_token) + except InvalidAuthError as exc: + return websocket_api.error_message( + msg["id"], websocket_api.const.ERR_UNAUTHORIZED, str(exc) + ) + + connection.send_message(websocket_api.result_message(msg["id"], access_token)) + + +@websocket_api.ws_require_user() +@callback +def websocket_refresh_tokens( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg +): + """Return metadata of users refresh tokens.""" + current_id = connection.refresh_token_id + connection.send_message( + websocket_api.result_message( + msg["id"], + [ + { + "id": refresh.id, + "client_id": refresh.client_id, + "client_name": refresh.client_name, + "client_icon": refresh.client_icon, + "type": refresh.token_type, + "created_at": refresh.created_at, + "is_current": refresh.id == current_id, + "last_used_at": refresh.last_used_at, + "last_used_ip": refresh.last_used_ip, + } + for refresh in connection.user.refresh_tokens.values() + ], + ) + ) + + +@websocket_api.ws_require_user() +@websocket_api.async_response +async def websocket_delete_refresh_token( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg +): + """Handle a delete refresh token request.""" + refresh_token = connection.user.refresh_tokens.get(msg["refresh_token_id"]) + + if refresh_token is None: + return websocket_api.error_message( + msg["id"], "invalid_token_id", "Received invalid token" + ) + + await hass.auth.async_remove_refresh_token(refresh_token) + + connection.send_message(websocket_api.result_message(msg["id"], {})) + + +@websocket_api.ws_require_user() +@callback +def websocket_sign_path( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg +): + """Handle a sign path request.""" + connection.send_message( + websocket_api.result_message( + msg["id"], + { + "path": async_sign_path( + hass, + connection.refresh_token_id, + msg["path"], + timedelta(seconds=msg["expires"]), + ) + }, + ) + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/indieauth.py b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/indieauth.py new file mode 100644 index 00000000000..e823659f62b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/indieauth.py @@ -0,0 +1,201 @@ +"""Helpers to resolve client ID/secret.""" +import asyncio +from html.parser import HTMLParser +from ipaddress import ip_address +import logging +from urllib.parse import urljoin, urlparse + +import aiohttp + +from homeassistant.util.network import is_local + +_LOGGER = logging.getLogger(__name__) + + +async def verify_redirect_uri(hass, client_id, redirect_uri): + """Verify that the client and redirect uri match.""" + try: + client_id_parts = _parse_client_id(client_id) + except ValueError: + return False + + redirect_parts = _parse_url(redirect_uri) + + # Verify redirect url and client url have same scheme and domain. + is_valid = ( + client_id_parts.scheme == redirect_parts.scheme + and client_id_parts.netloc == redirect_parts.netloc + ) + + if is_valid: + return True + + # Whitelist the iOS and Android callbacks so that people can link apps + # without being connected to the internet. + if redirect_uri == "homeassistant://auth-callback" and client_id in ( + "https://home-assistant.io/android", + "https://home-assistant.io/iOS", + ): + return True + + # IndieAuth 4.2.2 allows for redirect_uri to be on different domain + # but needs to be specified in link tag when fetching `client_id`. + redirect_uris = await fetch_redirect_uris(hass, client_id) + return redirect_uri in redirect_uris + + +class LinkTagParser(HTMLParser): + """Parser to find link tags.""" + + def __init__(self, rel): + """Initialize a link tag parser.""" + super().__init__() + self.rel = rel + self.found = [] + + def handle_starttag(self, tag, attrs): + """Handle finding a start tag.""" + if tag != "link": + return + + attrs = dict(attrs) + + if attrs.get("rel") == self.rel: + self.found.append(attrs.get("href")) + + +async def fetch_redirect_uris(hass, url): + """Find link tag with redirect_uri values. + + IndieAuth 4.2.2 + + The client SHOULD publish one or more tags or Link HTTP headers with + a rel attribute of redirect_uri at the client_id URL. + + We limit to the first 10kB of the page. + + We do not implement extracting redirect uris from headers. + """ + parser = LinkTagParser("redirect_uri") + chunks = 0 + try: + async with aiohttp.ClientSession() as session: + async with session.get(url, timeout=5) as resp: + async for data in resp.content.iter_chunked(1024): + parser.feed(data.decode()) + chunks += 1 + + if chunks == 10: + break + + except asyncio.TimeoutError: + _LOGGER.error("Timeout while looking up redirect_uri %s", url) + except aiohttp.client_exceptions.ClientSSLError: + _LOGGER.error("SSL error while looking up redirect_uri %s", url) + except aiohttp.client_exceptions.ClientOSError as ex: + _LOGGER.error("OS error while looking up redirect_uri %s: %s", url, ex.strerror) + except aiohttp.client_exceptions.ClientConnectionError: + _LOGGER.error( + "Low level connection error while looking up redirect_uri %s", url + ) + except aiohttp.client_exceptions.ClientError: + _LOGGER.error("Unknown error while looking up redirect_uri %s", url) + + # Authorization endpoints verifying that a redirect_uri is allowed for use + # by a client MUST look for an exact match of the given redirect_uri in the + # request against the list of redirect_uris discovered after resolving any + # relative URLs. + return [urljoin(url, found) for found in parser.found] + + +def verify_client_id(client_id): + """Verify that the client id is valid.""" + try: + _parse_client_id(client_id) + return True + except ValueError: + return False + + +def _parse_url(url): + """Parse a url in parts and canonicalize according to IndieAuth.""" + parts = urlparse(url) + + # Canonicalize a url according to IndieAuth 3.2. + + # SHOULD convert the hostname to lowercase + parts = parts._replace(netloc=parts.netloc.lower()) + + # If a URL with no path component is ever encountered, + # it MUST be treated as if it had the path /. + if parts.path == "": + parts = parts._replace(path="/") + + return parts + + +def _parse_client_id(client_id): + """Test if client id is a valid URL according to IndieAuth section 3.2. + + https://indieauth.spec.indieweb.org/#client-identifier + """ + parts = _parse_url(client_id) + + # Client identifier URLs + # MUST have either an https or http scheme + if parts.scheme not in ("http", "https"): + raise ValueError() + + # MUST contain a path component + # Handled by url canonicalization. + + # MUST NOT contain single-dot or double-dot path segments + if any(segment in (".", "..") for segment in parts.path.split("/")): + raise ValueError( + "Client ID cannot contain single-dot or double-dot path segments" + ) + + # MUST NOT contain a fragment component + if parts.fragment != "": + raise ValueError("Client ID cannot contain a fragment") + + # MUST NOT contain a username or password component + if parts.username is not None: + raise ValueError("Client ID cannot contain username") + + if parts.password is not None: + raise ValueError("Client ID cannot contain password") + + # MAY contain a port + try: + # parts raises ValueError when port cannot be parsed as int + parts.port + except ValueError as ex: + raise ValueError("Client ID contains invalid port") from ex + + # Additionally, hostnames + # MUST be domain names or a loopback interface and + # MUST NOT be IPv4 or IPv6 addresses except for IPv4 127.0.0.1 + # or IPv6 [::1] + + # We are not goint to follow the spec here. We are going to allow + # any internal network IP to be used inside a client id. + + address = None + + try: + netloc = parts.netloc + + # Strip the [, ] from ipv6 addresses before parsing + if netloc[0] == "[" and netloc[-1] == "]": + netloc = netloc[1:-1] + + address = ip_address(netloc) + except ValueError: + # Not an ip address + pass + + if address is None or is_local(address): + return parts + + raise ValueError("Hostname should be a domain name or local IP address") diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/login_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/login_flow.py new file mode 100644 index 00000000000..725450a0a12 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/login_flow.py @@ -0,0 +1,270 @@ +"""HTTP views handle login flow. + +# GET /auth/providers + +Return a list of auth providers. Example: + +[ + { + "name": "Local", + "id": null, + "type": "local_provider", + } +] + + +# POST /auth/login_flow + +Create a login flow. Will return the first step of the flow. + +Pass in parameter 'client_id' and 'redirect_url' validate by indieauth. + +Pass in parameter 'handler' to specify the auth provider to use. Auth providers +are identified by type and id. + +And optional parameter 'type' has to set as 'link_user' if login flow used for +link credential to exist user. Default 'type' is 'authorize'. + +{ + "client_id": "https://hassbian.local:8123/", + "handler": ["local_provider", null], + "redirect_url": "https://hassbian.local:8123/", + "type': "authorize" +} + +Return value will be a step in a data entry flow. See the docs for data entry +flow for details. + +{ + "data_schema": [ + {"name": "username", "type": "string"}, + {"name": "password", "type": "string"} + ], + "errors": {}, + "flow_id": "8f7e42faab604bcab7ac43c44ca34d58", + "handler": ["insecure_example", null], + "step_id": "init", + "type": "form" +} + + +# POST /auth/login_flow/{flow_id} + +Progress the flow. Most flows will be 1 page, but could optionally add extra +login challenges, like TFA. Once the flow has finished, the returned step will +have type "create_entry" and "result" key will contain an authorization code. +The authorization code associated with an authorized user by default, it will +associate with an credential if "type" set to "link_user" in +"/auth/login_flow" + +{ + "flow_id": "8f7e42faab604bcab7ac43c44ca34d58", + "handler": ["insecure_example", null], + "result": "411ee2f916e648d691e937ae9344681e", + "title": "Example", + "type": "create_entry", + "version": 1 +} +""" +from ipaddress import ip_address + +from aiohttp import web +import voluptuous as vol +import voluptuous_serialize + +from homeassistant import data_entry_flow +from homeassistant.components.http.ban import ( + log_invalid_auth, + process_success_login, + process_wrong_login, +) +from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.components.http.view import HomeAssistantView +from homeassistant.const import ( + HTTP_BAD_REQUEST, + HTTP_METHOD_NOT_ALLOWED, + HTTP_NOT_FOUND, +) + +from . import indieauth + + +async def async_setup(hass, store_result): + """Component to allow users to login.""" + hass.http.register_view(AuthProvidersView) + hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow, store_result)) + hass.http.register_view(LoginFlowResourceView(hass.auth.login_flow, store_result)) + + +class AuthProvidersView(HomeAssistantView): + """View to get available auth providers.""" + + url = "/auth/providers" + name = "api:auth:providers" + requires_auth = False + + async def get(self, request): + """Get available auth providers.""" + hass = request.app["hass"] + if not hass.components.onboarding.async_is_user_onboarded(): + return self.json_message( + message="Onboarding not finished", + status_code=HTTP_BAD_REQUEST, + message_code="onboarding_required", + ) + + return self.json( + [ + {"name": provider.name, "id": provider.id, "type": provider.type} + for provider in hass.auth.auth_providers + ] + ) + + +def _prepare_result_json(result): + """Convert result to JSON.""" + if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + data = result.copy() + data.pop("result") + data.pop("data") + return data + + if result["type"] != data_entry_flow.RESULT_TYPE_FORM: + return result + + data = result.copy() + + schema = data["data_schema"] + if schema is None: + data["data_schema"] = [] + else: + data["data_schema"] = voluptuous_serialize.convert(schema) + + return data + + +class LoginFlowIndexView(HomeAssistantView): + """View to create a config flow.""" + + url = "/auth/login_flow" + name = "api:auth:login_flow" + requires_auth = False + + def __init__(self, flow_mgr, store_result): + """Initialize the flow manager index view.""" + self._flow_mgr = flow_mgr + self._store_result = store_result + + async def get(self, request): + """Do not allow index of flows in progress.""" + return web.Response(status=HTTP_METHOD_NOT_ALLOWED) + + @RequestDataValidator( + vol.Schema( + { + vol.Required("client_id"): str, + vol.Required("handler"): vol.Any(str, list), + vol.Required("redirect_uri"): str, + vol.Optional("type", default="authorize"): str, + } + ) + ) + @log_invalid_auth + async def post(self, request, data): + """Create a new login flow.""" + if not await indieauth.verify_redirect_uri( + request.app["hass"], data["client_id"], data["redirect_uri"] + ): + return self.json_message( + "invalid client id or redirect uri", HTTP_BAD_REQUEST + ) + + if isinstance(data["handler"], list): + handler = tuple(data["handler"]) + else: + handler = data["handler"] + + try: + result = await self._flow_mgr.async_init( + handler, + context={ + "ip_address": ip_address(request.remote), + "credential_only": data.get("type") == "link_user", + }, + ) + except data_entry_flow.UnknownHandler: + return self.json_message("Invalid handler specified", HTTP_NOT_FOUND) + except data_entry_flow.UnknownStep: + return self.json_message("Handler does not support init", HTTP_BAD_REQUEST) + + if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + await process_success_login(request) + result.pop("data") + result["result"] = self._store_result(data["client_id"], result["result"]) + return self.json(result) + + return self.json(_prepare_result_json(result)) + + +class LoginFlowResourceView(HomeAssistantView): + """View to interact with the flow manager.""" + + url = "/auth/login_flow/{flow_id}" + name = "api:auth:login_flow:resource" + requires_auth = False + + def __init__(self, flow_mgr, store_result): + """Initialize the login flow resource view.""" + self._flow_mgr = flow_mgr + self._store_result = store_result + + async def get(self, request): + """Do not allow getting status of a flow in progress.""" + return self.json_message("Invalid flow specified", HTTP_NOT_FOUND) + + @RequestDataValidator(vol.Schema({"client_id": str}, extra=vol.ALLOW_EXTRA)) + @log_invalid_auth + async def post(self, request, flow_id, data): + """Handle progressing a login flow request.""" + client_id = data.pop("client_id") + + if not indieauth.verify_client_id(client_id): + return self.json_message("Invalid client id", HTTP_BAD_REQUEST) + + try: + # do not allow change ip during login flow + for flow in self._flow_mgr.async_progress(): + if flow["flow_id"] == flow_id and flow["context"][ + "ip_address" + ] != ip_address(request.remote): + return self.json_message("IP address changed", HTTP_BAD_REQUEST) + + result = await self._flow_mgr.async_configure(flow_id, data) + except data_entry_flow.UnknownFlow: + return self.json_message("Invalid flow specified", HTTP_NOT_FOUND) + except vol.Invalid: + return self.json_message("User input malformed", HTTP_BAD_REQUEST) + + if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + # @log_invalid_auth does not work here since it returns HTTP 200 + # need manually log failed login attempts + if result.get("errors") is not None and result["errors"].get("base") in [ + "invalid_auth", + "invalid_code", + ]: + await process_wrong_login(request) + return self.json(_prepare_result_json(result)) + + result.pop("data") + result["result"] = self._store_result(client_id, result["result"]) + + return self.json(result) + + async def delete(self, request, flow_id): + """Cancel a flow in progress.""" + try: + self._flow_mgr.async_abort(flow_id) + except data_entry_flow.UnknownFlow: + return self.json_message("Invalid flow specified", HTTP_NOT_FOUND) + + return self.json_message("Flow aborted") diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/manifest.json new file mode 100644 index 00000000000..2674bdfb032 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "auth", + "name": "Auth", + "documentation": "https://www.home-assistant.io/integrations/auth", + "dependencies": ["http"], + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/mfa_setup_flow.py new file mode 100644 index 00000000000..1b199551a14 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/mfa_setup_flow.py @@ -0,0 +1,148 @@ +"""Helpers to setup multi-factor auth module.""" +import logging + +import voluptuous as vol +import voluptuous_serialize + +from homeassistant import data_entry_flow +from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant, callback + +WS_TYPE_SETUP_MFA = "auth/setup_mfa" +SCHEMA_WS_SETUP_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + { + vol.Required("type"): WS_TYPE_SETUP_MFA, + vol.Exclusive("mfa_module_id", "module_or_flow_id"): str, + vol.Exclusive("flow_id", "module_or_flow_id"): str, + vol.Optional("user_input"): object, + } +) + +WS_TYPE_DEPOSE_MFA = "auth/depose_mfa" +SCHEMA_WS_DEPOSE_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_DEPOSE_MFA, vol.Required("mfa_module_id"): str} +) + +DATA_SETUP_FLOW_MGR = "auth_mfa_setup_flow_manager" + +_LOGGER = logging.getLogger(__name__) + + +class MfaFlowManager(data_entry_flow.FlowManager): + """Manage multi factor authentication flows.""" + + async def async_create_flow(self, handler_key, *, context, data): + """Create a setup flow. handler is a mfa module.""" + mfa_module = self.hass.auth.get_auth_mfa_module(handler_key) + if mfa_module is None: + raise ValueError(f"Mfa module {handler_key} is not found") + + user_id = data.pop("user_id") + return await mfa_module.async_setup_flow(user_id) + + async def async_finish_flow(self, flow, result): + """Complete an mfs setup flow.""" + _LOGGER.debug("flow_result: %s", result) + return result + + +async def async_setup(hass): + """Init mfa setup flow manager.""" + hass.data[DATA_SETUP_FLOW_MGR] = MfaFlowManager(hass) + + hass.components.websocket_api.async_register_command( + WS_TYPE_SETUP_MFA, websocket_setup_mfa, SCHEMA_WS_SETUP_MFA + ) + + hass.components.websocket_api.async_register_command( + WS_TYPE_DEPOSE_MFA, websocket_depose_mfa, SCHEMA_WS_DEPOSE_MFA + ) + + +@callback +@websocket_api.ws_require_user(allow_system_user=False) +def websocket_setup_mfa( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg +): + """Return a setup flow for mfa auth module.""" + + async def async_setup_flow(msg): + """Return a setup flow for mfa auth module.""" + flow_manager = hass.data[DATA_SETUP_FLOW_MGR] + + flow_id = msg.get("flow_id") + if flow_id is not None: + result = await flow_manager.async_configure(flow_id, msg.get("user_input")) + connection.send_message( + websocket_api.result_message(msg["id"], _prepare_result_json(result)) + ) + return + + mfa_module_id = msg.get("mfa_module_id") + mfa_module = hass.auth.get_auth_mfa_module(mfa_module_id) + if mfa_module is None: + connection.send_message( + websocket_api.error_message( + msg["id"], "no_module", f"MFA module {mfa_module_id} is not found" + ) + ) + return + + result = await flow_manager.async_init( + mfa_module_id, data={"user_id": connection.user.id} + ) + + connection.send_message( + websocket_api.result_message(msg["id"], _prepare_result_json(result)) + ) + + hass.async_create_task(async_setup_flow(msg)) + + +@callback +@websocket_api.ws_require_user(allow_system_user=False) +def websocket_depose_mfa( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg +): + """Remove user from mfa module.""" + + async def async_depose(msg): + """Remove user from mfa auth module.""" + mfa_module_id = msg["mfa_module_id"] + try: + await hass.auth.async_disable_user_mfa( + connection.user, msg["mfa_module_id"] + ) + except ValueError as err: + connection.send_message( + websocket_api.error_message( + msg["id"], + "disable_failed", + f"Cannot disable MFA Module {mfa_module_id}: {err}", + ) + ) + return + + connection.send_message(websocket_api.result_message(msg["id"], "done")) + + hass.async_create_task(async_depose(msg)) + + +def _prepare_result_json(result): + """Convert result to JSON.""" + if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + data = result.copy() + return data + + if result["type"] != data_entry_flow.RESULT_TYPE_FORM: + return result + + data = result.copy() + + schema = data["data_schema"] + if schema is None: + data["data_schema"] = [] + else: + data["data_schema"] = voluptuous_serialize.convert(schema) + + return data diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/strings.json new file mode 100644 index 00000000000..d386bb7a488 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/strings.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "totp": { + "title": "TOTP", + "step": { + "init": { + "title": "Set up two-factor authentication using TOTP", + "description": "To activate two factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**." + } + }, + "error": { + "invalid_code": "Invalid code, please try again. If you get this error consistently, please make sure the clock of your Home Assistant system is accurate." + } + }, + "notify": { + "title": "Notify One-Time Password", + "step": { + "init": { + "title": "Set up one-time password delivered by notify component", + "description": "Please select one of the notification services:" + }, + "setup": { + "title": "Verify setup", + "description": "A one-time password has been sent via **notify.{notify_service}**. Please enter it below:" + } + }, + "abort": { + "no_available_service": "No notification services available." + }, + "error": { + "invalid_code": "Invalid code, please try again." + } + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/ar.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/ar.json new file mode 100644 index 00000000000..1ef902e6fe2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/ar.json @@ -0,0 +1,7 @@ +{ + "mfa_setup": { + "totp": { + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/bg.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/bg.json new file mode 100644 index 00000000000..d07e20a854c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/bg.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043b\u0438\u0447\u043d\u0438 \u0443\u0441\u043b\u0443\u0433\u0438 \u0437\u0430 \u0443\u0432\u0435\u0434\u043e\u043c\u044f\u0432\u0430\u043d\u0435." + }, + "error": { + "invalid_code": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u043a\u043e\u0434, \u043c\u043e\u043b\u044f \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." + }, + "step": { + "init": { + "description": "\u041c\u043e\u043b\u044f, \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0435\u0434\u043d\u0430 \u043e\u0442 \u0443\u0441\u043b\u0443\u0433\u0438\u0442\u0435 \u0437\u0430 \u0443\u0432\u0435\u0434\u043e\u043c\u044f\u0432\u0430\u043d\u0435:", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435 \u043d\u0430 \u0435\u0434\u043d\u043e\u043a\u0440\u0430\u0442\u043d\u0430 \u043f\u0430\u0440\u043e\u043b\u0430, \u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d\u0430 \u0447\u0440\u0435\u0437 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0437\u0430 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435" + }, + "setup": { + "description": "\u0415\u0434\u043d\u043e\u043a\u0440\u0430\u0442\u043d\u0430 \u043f\u0430\u0440\u043e\u043b\u0430 \u0435 \u0438\u0437\u043f\u0440\u0430\u0442\u0435\u043d\u0430 \u0447\u0440\u0435\u0437 **notify.{notify_service}**. \u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u044f \u043f\u043e-\u0434\u043e\u043b\u0443:", + "title": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430" + } + }, + "title": "\u0423\u0432\u0435\u0434\u043e\u043c\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0435\u0434\u043d\u043e\u043a\u0440\u0430\u0442\u043d\u0430 \u043f\u0430\u0440\u043e\u043b\u0430" + }, + "totp": { + "error": { + "invalid_code": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u043a\u043e\u0434, \u043c\u043e\u043b\u044f \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e. \u0410\u043a\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430\u0442\u0435 \u0442\u0430\u0437\u0438 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e, \u043c\u043e\u043b\u044f, \u0443\u0432\u0435\u0440\u0435\u0442\u0435 \u0441\u0435, \u0447\u0435 \u0447\u0430\u0441\u043e\u0432\u043d\u0438\u043a\u044a\u0442 \u043d\u0430 Home Assistant \u0435 \u0441\u0432\u0435\u0440\u0435\u043d." + }, + "step": { + "init": { + "description": "\u0417\u0430 \u0434\u0430 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0442\u0435 \u0434\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u0432\u043e-\u0431\u0430\u0437\u0438\u0440\u0430\u043d\u0438 \u0435\u0434\u043d\u043e\u043a\u0440\u0430\u0442\u043d\u0438 \u043f\u0430\u0440\u043e\u043b\u0438, \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u0439\u0442\u0435 QR \u043a\u043e\u0434\u0430 \u0441 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0442\u043e\u0440\u0430. \u0410\u043a\u043e \u043d\u044f\u043c\u0430\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0412\u0438 \u043f\u0440\u0435\u043f\u043e\u0440\u044a\u0447\u0432\u0430\u043c\u0435 \u0438\u043b\u0438 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u0438\u043b\u0438 [Authy](https://authy.com/).\n\n{qr_code}\n\n\u0421\u043b\u0435\u0434 \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434\u0430, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 6-\u0442\u0435 \u0446\u0438\u0444\u0440\u0438 \u043e\u0442 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0437\u0430 \u0434\u0430 \u043f\u043e\u0442\u0432\u044a\u0440\u0434\u0438\u0442\u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435\u0442\u043e. \u0410\u043a\u043e \u0438\u043c\u0430\u0442\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u043f\u0440\u0438 \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 QR \u043a\u043e\u0434\u0430, \u043d\u0430\u043f\u0440\u0430\u0432\u0435\u0442\u0435 \u0440\u044a\u0447\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u043a\u043e\u0434 **`{code}`**.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043d\u0430 \u0434\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0447\u0440\u0435\u0437 TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/ca.json new file mode 100644 index 00000000000..e5ece421a0b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/ca.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "No hi ha serveis de notificaci\u00f3 disponibles." + }, + "error": { + "invalid_code": "Codi inv\u00e0lid, si us plau torna a provar-ho." + }, + "step": { + "init": { + "description": "Selecciona un dels serveis de notificaci\u00f3:", + "title": "Configuraci\u00f3 d'una contrasenya d'un sol \u00fas a trav\u00e9s del component de notificacions" + }, + "setup": { + "description": "S'ha enviat una contrasenya d'un sol \u00fas mitjan\u00e7ant **notify.{notify_service}**. Introdueix-la a continuaci\u00f3:", + "title": "Verificaci\u00f3 de la configuraci\u00f3" + } + }, + "title": "Contrasenya d'un sol \u00fas del servei de notificacions" + }, + "totp": { + "error": { + "invalid_code": "Codi inv\u00e0lid, si us plau torna a provar-ho. Si obtens aquest error repetidament, assegura't que la data i hora de Home Assistant siguin correctes i acurades." + }, + "step": { + "init": { + "description": "Per activar la verificaci\u00f3 en dos passos mitjan\u00e7ant contrasenyes d'un sol \u00fas basades en temps, escaneja el codi QR amb la teva aplicaci\u00f3 de verificaci\u00f3. Si no en tens cap, et recomanem [Google Authenticator](https://support.google.com/accounts/answer/1066447) o b\u00e9 [Authy](https://authy.com/). \n\n {qr_code} \n \nDespr\u00e9s d'escanejar el codi QR, introdueix el codi de sis d\u00edgits proporcionat per l'aplicaci\u00f3. Si tens problemes per escanejar el codi QR, fes una configuraci\u00f3 manual amb el codi **`{code}`**.", + "title": "Configura la verificaci\u00f3 en dos passos utilitzant TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/cs.json new file mode 100644 index 00000000000..f6ca546ef5a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/cs.json @@ -0,0 +1,34 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "\u017d\u00e1dn\u00e9 oznamovac\u00ed slu\u017eby nejsou k dispozici." + }, + "error": { + "invalid_code": "Neplatn\u00fd k\u00f3d, zkuste to znovu." + }, + "step": { + "init": { + "description": "Vyberte pros\u00edm jednu z oznamovac\u00edch slu\u017eeb:", + "title": "Nastavte jednor\u00e1zov\u00e9 heslo dodan\u00e9 komponentou notify" + }, + "setup": { + "description": "Jednor\u00e1zov\u00e9 heslo bylo odesl\u00e1no prost\u0159ednictv\u00edm **notify.{notify_service}**. Zadejte jej n\u00ed\u017ee:", + "title": "Ov\u011b\u0159en\u00ed nastaven\u00ed" + } + } + }, + "totp": { + "error": { + "invalid_code": "Neplatn\u00fd k\u00f3d, zkuste to znovu. Pokud se tato chyba opakuje, ujist\u011bte se, \u017ee hodiny syst\u00e9mu Home Assistant jsou spr\u00e1vn\u011b nastaveny." + }, + "step": { + "init": { + "description": "Chcete-li aktivovat dvoufaktorovou autentizaci pomoc\u00ed jednor\u00e1zov\u00fdch hesel zalo\u017een\u00fdch na \u010dase, na\u010dt\u011bte k\u00f3d QR pomoc\u00ed va\u0161\u00ed autentiza\u010dn\u00ed aplikace. Pokud ji nem\u00e1te, doporu\u010dujeme bu\u010f [Google Authenticator](https://support.google.com/accounts/answer/1066447) nebo [Authy](https://authy.com/). \n\n{qr_code} \n \nPo skenov\u00e1n\u00ed k\u00f3du zadejte \u0161estcifern\u00fd k\u00f3d z aplikace a ov\u011b\u0159te nastaven\u00ed. Pokud m\u00e1te probl\u00e9my se skenov\u00e1n\u00edm k\u00f3du QR, prove\u010fte ru\u010dn\u00ed nastaven\u00ed s k\u00f3dem **`{code}`**.", + "title": "Nastavte dvoufaktorovou ov\u011b\u0159ov\u00e1n\u00ed pomoc\u00ed TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/da.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/da.json new file mode 100644 index 00000000000..7877a813218 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/da.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Ingen meddelelsestjenester tilg\u00e6ngelige." + }, + "error": { + "invalid_code": "Ugyldig kode, pr\u00f8v venligst igen." + }, + "step": { + "init": { + "description": "V\u00e6lg venligst en af meddelelsestjenesterne:", + "title": "Ops\u00e6t engangsadgangskoder leveret af notify-komponenten" + }, + "setup": { + "description": "En engangsadgangskode er blevet sendt via **notify.{notify_service}**. Indtast den venligst nedenunder:", + "title": "Bekr\u00e6ft ops\u00e6tningen" + } + }, + "title": "Notify-engangsadgangskode" + }, + "totp": { + "error": { + "invalid_code": "Ugyldig kode, pr\u00f8v venligst igen. Hvis du konsekvent f\u00e5r denne fejl skal du s\u00f8rge for at uret p\u00e5 dit Home Assistant system er g\u00e5r n\u00f8jagtigt." + }, + "step": { + "init": { + "description": "Hvis du vil aktivere tofaktorgodkendelse ved hj\u00e6lp af tidsbaserede engangskoder skal du scanne QR-koden med din autentificeringsapp. Hvis du ikke har en anbefaler vi enten [Google Authenticator] (https://support.google.com/accounts/answer/1066447) eller [Authy] (https://authy.com/). \n\n {qr_code} \n \nN\u00e5r du har scannet koden skal du indtaste den sekscifrede kode fra din app for at bekr\u00e6fte ops\u00e6tningen. Hvis du har problemer med at scanne QR-koden skal du lave en manuel ops\u00e6tning med kode **`{code}`**.", + "title": "Konfigurer tofaktorgodkendelse ved hj\u00e6lp af TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/de.json new file mode 100644 index 00000000000..93cbf1073cc --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/de.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Keine Benachrichtigungsdienste verf\u00fcgbar." + }, + "error": { + "invalid_code": "Ung\u00fcltiger Code, bitte versuche es erneut." + }, + "step": { + "init": { + "description": "Bitte w\u00e4hlen Sie einen der Benachrichtigungsdienste:", + "title": "Einmal Passwort f\u00fcr Notify einrichten" + }, + "setup": { + "description": "Ein Einmal-Passwort wurde per **notify.{notify_service}** gesendet. Bitte geben Sie es unten ein:", + "title": "\u00dcberpr\u00fcfe das Setup" + } + }, + "title": "Benachrichtig f\u00fcr One-Time Password" + }, + "totp": { + "error": { + "invalid_code": "Ung\u00fcltiger Code, bitte versuche es erneut. Wenn du diesen Fehler regelm\u00e4\u00dfig erhalten, stelle sicher, dass die Uhr deines Home Assistant-Systems korrekt ist." + }, + "step": { + "init": { + "description": "Um die Zwei-Faktor-Authentifizierung mit zeitbasierten Einmalpassw\u00f6rtern zu aktivieren, scanne den QR-Code mit deiner Authentifizierungs-App. Wenn du keine hast, empfehlen wir entweder [Google Authenticator] (https://support.google.com/accounts/answer/1066447) oder [Authy] (https://authy.com/). \n\n {qr_code} \n \nNachdem du den Code gescannt hast, gibst du den sechsstelligen Code aus der App ein, um das Setup zu \u00fcberpr\u00fcfen. Wenn es Probleme beim Scannen des QR-Codes gibt, f\u00fchre ein manuelles Setup mit dem Code ** ` {code} ` ** durch.", + "title": "Richte die Zwei-Faktor-Authentifizierung mit TOTP ein" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/en.json new file mode 100644 index 00000000000..66c0e92d9b5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/en.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "No notification services available." + }, + "error": { + "invalid_code": "Invalid code, please try again." + }, + "step": { + "init": { + "description": "Please select one of the notification services:", + "title": "Set up one-time password delivered by notify component" + }, + "setup": { + "description": "A one-time password has been sent via **notify.{notify_service}**. Please enter it below:", + "title": "Verify setup" + } + }, + "title": "Notify One-Time Password" + }, + "totp": { + "error": { + "invalid_code": "Invalid code, please try again. If you get this error consistently, please make sure the clock of your Home Assistant system is accurate." + }, + "step": { + "init": { + "description": "To activate two factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**.", + "title": "Set up two-factor authentication using TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/es-419.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/es-419.json new file mode 100644 index 00000000000..4ac97068905 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/es-419.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "No hay servicios de notificaci\u00f3n disponibles." + }, + "error": { + "invalid_code": "C\u00f3digo inv\u00e1lido, por favor int\u00e9ntelo de nuevo." + }, + "step": { + "init": { + "description": "Por favor seleccione uno de los servicios de notificaci\u00f3n:", + "title": "Configure la contrase\u00f1a de un solo uso entregada por el componente de notificaci\u00f3n" + }, + "setup": { + "description": "Se ha enviado una contrase\u00f1a \u00fanica a trav\u00e9s de **notify.{notify_service}**. Por favor ingr\u00e9selo a continuaci\u00f3n:", + "title": "Verificar la configuracion" + } + }, + "title": "Notificar contrase\u00f1a de un solo uso" + }, + "totp": { + "error": { + "invalid_code": "C\u00f3digo no v\u00e1lido, por favor vuelva a intentarlo. Si recibe este error constantemente, aseg\u00farese de que el reloj de su sistema Home Assistant sea exacto." + }, + "step": { + "init": { + "description": "Para activar la autenticaci\u00f3n de dos factores utilizando contrase\u00f1as de un solo uso basadas en el tiempo, escanee el c\u00f3digo QR con su aplicaci\u00f3n de autenticaci\u00f3n. Si no tiene uno, le recomendamos [Autenticador de Google] (https://support.google.com/accounts/answer/1066447) o [Authy] (https://authy.com/). \n\n {qr_code} \n \n Despu\u00e9s de escanear el c\u00f3digo, ingrese el c\u00f3digo de seis d\u00edgitos de su aplicaci\u00f3n para verificar la configuraci\u00f3n. Si tiene problemas para escanear el c\u00f3digo QR, realice una configuraci\u00f3n manual con el c\u00f3digo ** ` {code} ` **.", + "title": "Configurar la autenticaci\u00f3n de dos factores mediante TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/es.json new file mode 100644 index 00000000000..7495ffcfbc9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/es.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "No hay servicios de notificaci\u00f3n disponibles." + }, + "error": { + "invalid_code": "C\u00f3digo inv\u00e1lido, por favor int\u00e9ntelo de nuevo." + }, + "step": { + "init": { + "description": "Selecciona uno de los servicios de notificaci\u00f3n:", + "title": "Configurar una contrase\u00f1a de un solo uso entregada por el componente de notificaci\u00f3n" + }, + "setup": { + "description": "Se ha enviado una contrase\u00f1a de un solo uso a trav\u00e9s de ** notify.{notify_service} **. Por favor introd\u00facela a continuaci\u00f3n:", + "title": "Verificar la configuraci\u00f3n" + } + }, + "title": "Notificar la contrase\u00f1a de un solo uso" + }, + "totp": { + "error": { + "invalid_code": "C\u00f3digo inv\u00e1lido, int\u00e9ntalo de nuevo. Si recibes este error de forma consistente, aseg\u00farate de que el reloj de tu sistema Home Assistant es correcto." + }, + "step": { + "init": { + "description": "Para activar la autenticaci\u00f3n de dos factores utilizando contrase\u00f1as de un solo uso basadas en el tiempo, escanea el c\u00f3digo QR con tu aplicaci\u00f3n de autenticaci\u00f3n. Si no tienes una, te recomendamos el [Autenticador de Google](https://support.google.com/accounts/answer/1066447) o [Authy](https://authy.com/). \n\n {qr_code} \n \nDespu\u00e9s de escanear el c\u00f3digo, introduce el c\u00f3digo de seis d\u00edgitos de tu aplicaci\u00f3n para verificar la configuraci\u00f3n. Si tienes problemas para escanear el c\u00f3digo QR, realiza una configuraci\u00f3n manual con el c\u00f3digo **`{code}`**.", + "title": "Configurar la autenticaci\u00f3n de dos factores usando TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/et.json new file mode 100644 index 00000000000..9b22951e7fa --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/et.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Teavitusteenused pole saadaval." + }, + "error": { + "invalid_code": "Vale kood, palun proovi uuesti." + }, + "step": { + "init": { + "description": "Vali \u00fcks teavitusteenustest:", + "title": "Seadista Notify poolt edastatud \u00fchekordne parool" + }, + "setup": { + "description": "\u00dchekordne parool on saadetud **notify. {notify_service}**. Palun sisesta see allpool:", + "title": "Kontrolli seadistust" + } + }, + "title": "Notify \u00fchekordne parool" + }, + "totp": { + "error": { + "invalid_code": "Vale kood, palun proovi uuesti. Kui saad selle vea pidevalt, veendu, et Home Assistant-i s\u00fcsteemi kell oleks t\u00e4pne." + }, + "step": { + "init": { + "description": "Kahefaktorilise autentimise aktiveerimiseks ajap\u00f5histe \u00fchekordsete paroolide abil skanni QR-kood oma autentimisrakendusega. Kui seda pole, soovitame kas [Google Authenticator] (https://support.google.com/accounts/answer/1066447) v\u00f5i [Authy] (https://authy.com/).\n\n {qr_code}\n\n P\u00e4rast koodi skannimist sisesta seadistuse kinnitamiseks rakenduse kuuekohaline kood. Kui on probleeme QR-koodi skannimisega, tehke koodiga **' {code}' ** k\u00e4sitsi seadistamine.", + "title": "Seadista TOTP-ga kaheastmeline autentimine" + } + }, + "title": "" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/fi.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/fi.json new file mode 100644 index 00000000000..92e4f03c0f9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/fi.json @@ -0,0 +1,18 @@ +{ + "mfa_setup": { + "notify": { + "error": { + "invalid_code": "Virheellinen koodi. Yrit\u00e4 uudelleen." + }, + "step": { + "setup": { + "title": "Varmista asetukset" + } + }, + "title": "Ilmoita kertaluonteinen salasana" + }, + "totp": { + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/fr.json new file mode 100644 index 00000000000..cf0a1888495 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/fr.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Aucun service de notification disponible." + }, + "error": { + "invalid_code": "Code invalide. Veuillez essayer \u00e0 nouveau." + }, + "step": { + "init": { + "description": "Veuillez s\u00e9lectionner l'un des services de notification:", + "title": "Configurer un mot de passe \u00e0 usage unique d\u00e9livr\u00e9 par le composant notify" + }, + "setup": { + "description": "Un mot de passe unique a \u00e9t\u00e9 envoy\u00e9 par **notify.{notify_service}**. Veuillez le saisir ci-dessous :", + "title": "V\u00e9rifier la configuration" + } + }, + "title": "Notifier un mot de passe unique" + }, + "totp": { + "error": { + "invalid_code": "Code invalide. Veuillez essayez \u00e0 nouveau. Si cette erreur persiste, assurez-vous que l'horloge de votre syst\u00e8me Home Assistant est correcte." + }, + "step": { + "init": { + "description": "Pour activer l'authentification \u00e0 deux facteurs \u00e0 l'aide de mots de passe \u00e0 utilisation unique bas\u00e9s sur l'heure, num\u00e9risez le code QR avec votre application d'authentification. Si vous n'en avez pas, nous vous recommandons d'utiliser [Google Authenticator] (https://support.google.com/accounts/answer/1066447) ou [Authy] (https://authy.com/). \n\n {qr_code} \n \n Apr\u00e8s avoir num\u00e9ris\u00e9 le code, entrez le code \u00e0 six chiffres de votre application pour v\u00e9rifier la configuration. Si vous rencontrez des probl\u00e8mes lors de l\u2019analyse du code QR, effectuez une configuration manuelle avec le code ** ` {code} ` **.", + "title": "Configurer une authentification \u00e0 deux facteurs \u00e0 l'aide de TOTP" + } + }, + "title": "TOTP (Mot de passe \u00e0 utilisation unique bas\u00e9 sur le temps)" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/he.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/he.json new file mode 100644 index 00000000000..bc1826d4d79 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/he.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "\u05d0\u05d9\u05df \u05e9\u05d9\u05e8\u05d5\u05ea\u05d9 notify \u05d6\u05de\u05d9\u05e0\u05d9\u05dd." + }, + "error": { + "invalid_code": "\u05e7\u05d5\u05d3 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1." + }, + "step": { + "init": { + "description": "\u05d1\u05d7\u05e8 \u05d0\u05ea \u05d0\u05d7\u05d3 \u05de\u05e9\u05e8\u05d5\u05ea\u05d9 notify", + "title": "\u05d4\u05d2\u05d3\u05e8 \u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea \u05d4\u05e0\u05e9\u05dc\u05d7\u05ea \u05e2\u05dc \u05d9\u05d3\u05d9 \u05e8\u05db\u05d9\u05d1 notify" + }, + "setup": { + "description": "\u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea \u05e0\u05e9\u05dc\u05d7\u05d4 \u05e2\u05dc \u05d9\u05d3\u05d9 **{notify_service}**. \u05d4\u05d6\u05df \u05d0\u05d5\u05ea\u05d4 \u05dc\u05de\u05d8\u05d4:", + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d4\u05d4\u05ea\u05e7\u05e0\u05d4" + } + }, + "title": "\u05dc\u05d4\u05d5\u05d3\u05d9\u05e2 \u200b\u200b\u05e2\u05dc \u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea" + }, + "totp": { + "error": { + "invalid_code": "\u05e7\u05d5\u05d3 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1. \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e7\u05d1\u05dc \u05d0\u05ea \u05d4\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d4\u05d6\u05d5 \u05d1\u05d0\u05d5\u05e4\u05df \u05e2\u05e7\u05d1\u05d9, \u05d5\u05d3\u05d0 \u05e9\u05d4\u05e9\u05e2\u05d5\u05df \u05e9\u05dc \u05de\u05e2\u05e8\u05db\u05ea \u05d4 - Home Assistant \u05e9\u05dc\u05da \u05de\u05d3\u05d5\u05d9\u05e7." + }, + "step": { + "init": { + "description": "\u05db\u05d3\u05d9 \u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e1\u05d9\u05e1\u05de\u05d0\u05d5\u05ea \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05d5\u05ea \u05de\u05d1\u05d5\u05e1\u05e1\u05d5\u05ea \u05d6\u05de\u05df, \u05e1\u05e8\u05d5\u05e7 \u05d0\u05ea \u05e7\u05d5\u05d3 QR \u05e2\u05dd \u05d9\u05d9\u05e9\u05d5\u05dd \u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05e9\u05dc\u05da. \u05d0\u05dd \u05d0\u05d9\u05df \u05dc\u05da \u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d6\u05d4, \u05d0\u05e0\u05d5 \u05de\u05de\u05dc\u05d9\u05e6\u05d9\u05dd \u05e2\u05dc [Google Authenticator] (https://support.google.com/accounts/answer/1066447) \u05d0\u05d5 [Authy] (https://authy.com/). \n\n {qr_code} \n \n \u05dc\u05d0\u05d7\u05e8 \u05e1\u05e8\u05d9\u05e7\u05ea \u05d4\u05e7\u05d5\u05d3, \u05d4\u05d6\u05df \u05d0\u05ea \u05d4\u05e7\u05d5\u05d3 \u05d1\u05df \u05e9\u05e9 \u05d4\u05e1\u05e4\u05e8\u05d5\u05ea \u05de\u05d4\u05d0\u05e4\u05dc\u05d9\u05e7\u05e6\u05d9\u05d4 \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05d0\u05de\u05ea \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d4. \u05d0\u05dd \u05d0\u05ea\u05d4 \u05e0\u05ea\u05e7\u05dc \u05d1\u05d1\u05e2\u05d9\u05d5\u05ea \u05d1\u05e1\u05e8\u05d9\u05e7\u05ea \u05e7\u05d5\u05d3 QR, \u05d1\u05e6\u05e2 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d9\u05d3\u05e0\u05d9\u05ea \u05e2\u05dd \u05e7\u05d5\u05d3 **`{code}`**.", + "title": "\u05d4\u05d2\u05d3\u05e8 \u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/hu.json new file mode 100644 index 00000000000..5e7b1835093 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/hu.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Nincs el\u00e9rhet\u0151 \u00e9rtes\u00edt\u00e9si szolg\u00e1ltat\u00e1s." + }, + "error": { + "invalid_code": "\u00c9rv\u00e9nytelen k\u00f3d, pr\u00f3b\u00e1ld \u00fajra." + }, + "step": { + "init": { + "description": "K\u00e9rlek, v\u00e1lassz egyet az \u00e9rtes\u00edt\u00e9si szolg\u00e1ltat\u00e1sok k\u00f6z\u00fcl:", + "title": "\u00c1ll\u00edtsa be az \u00e9rtes\u00edt\u00e9si \u00f6sszetev\u0151 \u00e1ltal megadott egyszeri jelsz\u00f3t" + }, + "setup": { + "description": "Az egyszeri jelsz\u00f3 el lett k\u00fcldve a(z) **notify.{notify_service}** szolg\u00e1ltat\u00e1ssal. K\u00e9rlek, add meg al\u00e1bb:", + "title": "Be\u00e1ll\u00edt\u00e1s ellen\u0151rz\u00e9se" + } + }, + "title": "Egyszeri Jelsz\u00f3 \u00c9rtes\u00edt\u00e9s" + }, + "totp": { + "error": { + "invalid_code": "\u00c9rv\u00e9nytelen k\u00f3d, pr\u00f3b\u00e1ld \u00fajra. Ha ez a hiba folyamatosan el\u0151fordul, akkor gy\u0151z\u0151dj meg r\u00f3la, hogy a Home Assistant rendszered \u00f3r\u00e1ja pontosan j\u00e1r." + }, + "step": { + "init": { + "description": "Ahhoz, hogy haszn\u00e1lhasd a k\u00e9tfaktoros hiteles\u00edt\u00e9st id\u0151alap\u00fa egyszeri jelszavakkal, szkenneld be a QR k\u00f3dot a hiteles\u00edt\u00e9si applik\u00e1ci\u00f3ddal. Ha m\u00e9g nincsen, akkor a [Google Hiteles\u00edt\u0151](https://support.google.com/accounts/answer/1066447)t vagy az [Authy](https://authy.com/)-t aj\u00e1nljuk.\n\n{qr_code}\n\nA k\u00f3d beolvas\u00e1sa ut\u00e1n add meg a hat sz\u00e1mjegy\u0171 k\u00f3dot az applik\u00e1ci\u00f3b\u00f3l a telep\u00edt\u00e9s ellen\u0151rz\u00e9s\u00e9hez. Ha probl\u00e9m\u00e1ba \u00fctk\u00f6z\u00f6l a QR k\u00f3d beolvas\u00e1s\u00e1n\u00e1l, akkor ind\u00edts egy k\u00e9zi be\u00e1ll\u00edt\u00e1st a **`{code}`** k\u00f3ddal.", + "title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s be\u00e1ll\u00edt\u00e1sa TOTP haszn\u00e1lat\u00e1val" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/id.json new file mode 100644 index 00000000000..ed7bede5fff --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/id.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Tidak ada layanan notifikasi yang tersedia." + }, + "error": { + "invalid_code": "Kode tifak valid, coba lagi." + }, + "step": { + "init": { + "description": "Pilih salah satu layanan notifikasi:", + "title": "Siapkan kata sandi sekali pakai yang dikirimkan oleh komponen notify" + }, + "setup": { + "description": "Kata sandi sekali pakai telah dikirim melalui **notify.{notify_service}**. Masukkan di bawah ini:", + "title": "Verifikasi penyiapan" + } + }, + "title": "Kata Sandi Sekali Pakai Notifikasi" + }, + "totp": { + "error": { + "invalid_code": "Kode tidak valid, coba lagi. Jika Anda terus mendapatkan kesalahan yang sama, pastikan jam pada sistem Home Assistant Anda sudah akurat." + }, + "step": { + "init": { + "description": "Untuk mengaktifkan autentikasi dua faktor menggunakan kata sandi sekali pakai berbasis waktu, pindai kode QR dengan aplikasi autentikasi Anda. Jika tidak punya, kami menyarankan aplikasi [Google Authenticator] (https://support.google.com/accounts/answer/1066447) atau [Authy] (https://authy.com/). \n\n {qr_code} \n \nSetelah memindai kode, masukkan kode enam digit dari aplikasi Anda untuk memverifikasi penyiapan. Jika mengalami masalah saat memindai kode QR, lakukan penyiapan manual dengan kode **`{code}`**.", + "title": "Siapkan autentikasi dua faktor menggunakan TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/it.json new file mode 100644 index 00000000000..34d404f0bc6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/it.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Nessun servizio di notifica disponibile." + }, + "error": { + "invalid_code": "Codice non valido, per favore riprovare." + }, + "step": { + "init": { + "description": "Selezionare uno dei servizi di notifica:", + "title": "Imposta la password monouso fornita dal componente di notifica" + }, + "setup": { + "description": "\u00c8 stata inviata una password monouso tramite **notify.{notify_service}**. Per favore, inseriscila qui sotto:", + "title": "Verifica l'installazione" + } + }, + "title": "Notifica la Password monouso" + }, + "totp": { + "error": { + "invalid_code": "Codice non valido, per favore riprovare. Se riscontri spesso questo errore, assicurati che l'orologio del sistema Home Assistant sia accurato." + }, + "step": { + "init": { + "description": "Per attivare l'autenticazione a due fattori utilizzando le password monouso basate sul tempo, eseguire la scansione del codice QR con l'app di autenticazione. Se non ne hai uno, ti consigliamo [Google Authenticator](https://support.google.com/accounts/answer/1066447) o [Authy](https://authy.com/). \n\n {qr_code} \n \nDopo la scansione, inserisci il codice a sei cifre dalla tua app per verificare la configurazione. Se riscontri problemi con la scansione del codice QR, esegui una configurazione manuale con il codice **`{code}`**.", + "title": "Imposta l'autenticazione a due fattori usando TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/ko.json new file mode 100644 index 00000000000..09af8eb89bf --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/ko.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c \uc54c\ub9bc \uc11c\ube44\uc2a4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "error": { + "invalid_code": "\ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." + }, + "step": { + "init": { + "description": "\uc54c\ub9bc \uc11c\ube44\uc2a4 \uc911 \ud558\ub098\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694:", + "title": "\uc54c\ub9bc \uad6c\uc131\uc694\uc18c\uac00 \uc81c\uacf5\ud558\ub294 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638 \uc124\uc815\ud558\uae30" + }, + "setup": { + "description": "**notify.{notify_service}**\uc5d0\uc11c \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \ubcf4\ub0c8\uc2b5\ub2c8\ub2e4. \uc544\ub798\uc758 \uacf5\ub780\uc5d0 \uc785\ub825\ud574\uc8fc\uc138\uc694:", + "title": "\uc124\uc815 \ud655\uc778\ud558\uae30" + } + }, + "title": "\uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638 \uc54c\ub9bc" + }, + "totp": { + "error": { + "invalid_code": "\ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \uc774 \uc624\ub958\uac00 \uc9c0\uc18d\uc801\uc73c\ub85c \ubc1c\uc0dd\ud55c\ub2e4\uba74 Home Assistant\uc758 \uc2dc\uac04\uc124\uc815\uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694." + }, + "step": { + "init": { + "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \uad6c\uc131\ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/)\ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud558\uc5ec \uc124\uc815\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\uc8fc\uc138\uc694.", + "title": "TOTP 2\ub2e8\uacc4 \uc778\uc99d \uad6c\uc131\ud558\uae30" + } + }, + "title": "TOTP (\uc2dc\uac04 \uae30\ubc18 OTP)" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/lb.json new file mode 100644 index 00000000000..12ced930446 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/lb.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Keen Notifikatioun's D\u00e9ngscht disponibel." + }, + "error": { + "invalid_code": "Ong\u00ebltege Code, prob\u00e9iert w.e.g. nach emol." + }, + "step": { + "init": { + "description": "Wielt w.e.g. een Notifikatioun's D\u00e9ngscht aus:", + "title": "Eemolegt Passwuert ariichte wat vun engem Notifikatioun's Komponente versch\u00e9ckt g\u00ebtt" + }, + "setup": { + "description": "Een eemolegt Passwuert ass vun **notify.{notify_service}** gesch\u00e9ckt ginn. Gitt et w.e.g hei \u00ebnnen dr\u00ebnner an:", + "title": "Astellungen iwwerpr\u00e9iwen" + } + }, + "title": "Eemolegt Passwuert Notifikatioun" + }, + "totp": { + "error": { + "invalid_code": "Ong\u00ebltege Login, prob\u00e9iert w.e.g. nach emol. Falls d\u00ebse Feeler Message \u00ebmmer er\u00ebm optr\u00ebtt dann iwwerpr\u00e9ift op d'Z\u00e4it vum Home Assistant System richteg ass." + }, + "step": { + "init": { + "description": "Fir d'Zwee-Faktor-Authentifikatioun m\u00ebttels engem Z\u00e4it bas\u00e9ierten eemolege Passwuert z'aktiv\u00e9ieren, scannt de QR Code mat enger Authentifikatioun's App.\nFalls dir keng hutt, recommand\u00e9iere mir entweder [Google Authenticator](https://support.google.com/accounts/answer/1066447) oder [Authy](https://authy.com/).\n\n{qr_code}\n\nNodeems de Code gescannt ass, gitt de sechs stellege Code vun der App a fir d'Konfiguratioun z'iwwerpr\u00e9iwen. Am Fall vu Problemer fir de QR Code ze scannen, gitt de folgende Code **`{code}`** a fir ee manuelle Setup.", + "title": "Zwee Faktor Authentifikatioun mat TOTP konfigur\u00e9ieren" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/nl.json new file mode 100644 index 00000000000..d61613097dd --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/nl.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Geen meldingsservices beschikbaar." + }, + "error": { + "invalid_code": "Ongeldige code, probeer opnieuw." + }, + "step": { + "init": { + "description": "Selecteer een van de meldingsservices:", + "title": "Stel een \u00e9\u00e9nmalig wachtwoord in dat wordt afgegeven door een meldingscomponent" + }, + "setup": { + "description": "Een \u00e9\u00e9nmalig wachtwoord is verzonden via **notify. {notify_service}**. Voer het hieronder in:", + "title": "Controleer de instellingen" + } + }, + "title": "Eenmalig wachtwoord melden" + }, + "totp": { + "error": { + "invalid_code": "Ongeldige code, probeer het opnieuw. Als u deze fout blijft krijgen, controleer dan of de klok van uw Home Assistant systeem correct is ingesteld." + }, + "step": { + "init": { + "description": "Voor het activeren van twee-factor-authenticatie via tijdgebonden eenmalige wachtwoorden: scan de QR code met uw authenticatie-app. Als u nog geen app heeft, adviseren we [Google Authenticator (https://support.google.com/accounts/answer/1066447) of [Authy](https://authy.com/).\n\n{qr_code}\n\nNa het scannen van de code voert u de zescijferige code uit uw app in om de instelling te controleren. Als u problemen heeft met het scannen van de QR-code, voert u een handmatige configuratie uit met code **`{code}`**.", + "title": "Configureer twee-factor-authenticatie via TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/nn.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/nn.json new file mode 100644 index 00000000000..346c1cfe0c7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/nn.json @@ -0,0 +1,16 @@ +{ + "mfa_setup": { + "totp": { + "error": { + "invalid_code": "Ugyldig kode, pr\u00f8v igjen. Dersom du heile tida f\u00e5r denne feilen, m\u00e5 du s\u00f8rge for at klokka p\u00e5 Home Assistant-systemet ditt er n\u00f8yaktig." + }, + "step": { + "init": { + "description": "For \u00e5 aktivere to-faktor-autentisering ved hjelp av tid-baserte eingangspassord, skann QR-koden med autentiseringsappen din. Dersom du ikkje har ein, vil vi r\u00e5de deg til \u00e5 bruke anten [Google Authenticator] (https://support.google.com/accounts/answer/1066447) eller [Authy] (https://authy.com/). \n\n {qr_code} \n \nN\u00e5r du har skanna koda, skriv du inn den sekssifra koda fr\u00e5 appen din for \u00e5 stadfeste oppsettet. Dersom du har problemer med \u00e5 skanne QR-koda, gjer du eit manuelt oppsett med kode ** ` {code} ` **.", + "title": "Konfigurer to-faktor-autentisering ved hjelp av TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/no.json new file mode 100644 index 00000000000..c0252e045b2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/no.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Ingen varslingstjenester er tilgjengelig" + }, + "error": { + "invalid_code": "Ugyldig kode, vennligst pr\u00f8v igjen" + }, + "step": { + "init": { + "description": "Vennligst velg en av varslingstjenestene:", + "title": "Sett opp engangspassord levert av varsel komponent" + }, + "setup": { + "description": "Et engangspassord har blitt sendt via **notify.{notify_service}**. Vennligst skriv det inn nedenfor:", + "title": "Bekreft oppsettet" + } + }, + "title": "Varsle engangspassord" + }, + "totp": { + "error": { + "invalid_code": "Ugyldig kode, pr\u00f8v igjen. Hvis du f\u00e5r denne feilen konsekvent, m\u00e5 du s\u00f8rge for at klokken p\u00e5 Home Assistant systemet er riktig." + }, + "step": { + "init": { + "description": "For \u00e5 aktivere totrinnsbekreftelse ved hjelp av tidsbaserte engangspassord, skann QR-koden med godkjenningsappen din. Hvis du ikke har en, anbefaler vi enten [Google Authenticator](https://support.google.com/accounts/answer/1066447) eller [Authy](https://authy.com/).\n\n {qr_code} \n \nEtter at du har skannet koden, angir du den seks-sifrede koden fra appen din for \u00e5 kontrollere oppsettet. Dersom du har problemer med \u00e5 skanne QR-koden kan du fylle inn f\u00f8lgende kode manuelt: **`{code}`**.", + "title": "Sett opp totrinnsbekreftelse ved hjelp av TOTP" + } + }, + "title": "" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/pl.json new file mode 100644 index 00000000000..e0d4db0e171 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/pl.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Brak dost\u0119pnych us\u0142ug powiadamiania" + }, + "error": { + "invalid_code": "Nieprawid\u0142owy kod, spr\u00f3buj ponownie" + }, + "step": { + "init": { + "description": "Prosz\u0119 wybra\u0107 jedn\u0105 us\u0142ug\u0119 powiadamiania:", + "title": "Konfiguracja jednorazowego has\u0142a dostarczonego przez komponent powiadomie\u0144" + }, + "setup": { + "description": "Has\u0142o jednorazowe zosta\u0142o wys\u0142ane przez **notify.{notify_service}**. Wprowad\u017a je poni\u017cej:", + "title": "Sprawd\u017a konfiguracj\u0119" + } + }, + "title": "Powiadomienie z has\u0142em jednorazowym" + }, + "totp": { + "error": { + "invalid_code": "Nieprawid\u0142owy kod, spr\u00f3buj ponownie. Je\u015bli b\u0142\u0105d b\u0119dzie si\u0119 powtarza\u0142, upewnij si\u0119, \u017ce czas zegara systemu Home Assistant jest prawid\u0142owy." + }, + "step": { + "init": { + "description": "Aby aktywowa\u0107 uwierzytelnianie dwusk\u0142adnikowe przy u\u017cyciu jednorazowych hase\u0142 opartych na czasie, zeskanuj kod QR za pomoc\u0105 aplikacji uwierzytelniaj\u0105cej. Je\u015bli jej nie masz, polecamy [Google Authenticator](https://support.google.com/accounts/answer/1066447) lub [Authy](https://authy.com/).\n\n{qr_code} \n \nPo zeskanowaniu kodu wprowad\u017a sze\u015bciocyfrowy kod z aplikacji, aby zweryfikowa\u0107 konfiguracj\u0119. Je\u015bli masz problemy z zeskanowaniem kodu QR, wykonaj r\u0119czn\u0105 konfiguracj\u0119 z kodem **`{code}`**.", + "title": "Konfiguracja uwierzytelniania dwusk\u0142adnikowego za pomoc\u0105 hase\u0142 jednorazowych opartych na czasie (TOTP)" + } + }, + "title": "Has\u0142a jednorazowe oparte na czasie" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/pt-BR.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/pt-BR.json new file mode 100644 index 00000000000..e08c27a32e6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/pt-BR.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Nenhum servi\u00e7o de notifica\u00e7\u00e3o dispon\u00edvel." + }, + "error": { + "invalid_code": "C\u00f3digo inv\u00e1lido, por favor tente novamente." + }, + "step": { + "init": { + "description": "Por favor, selecione um dos servi\u00e7os de notifica\u00e7\u00e3o:", + "title": "Configurar a senha de uso \u00fanico entregue pelo componente de notifica\u00e7\u00e3o" + }, + "setup": { + "description": "A senha de uso \u00fanico foi enviada via ** notify. {notify_service} **. Por favor, insira abaixo:", + "title": "Verificar a configura\u00e7\u00e3o" + } + }, + "title": "Notificar a senha de uso \u00fanico" + }, + "totp": { + "error": { + "invalid_code": "C\u00f3digo inv\u00e1lido, por favor tente novamente. Se voc\u00ea obtiver este erro de forma consistente, certifique-se de que o rel\u00f3gio do sistema Home Assistant esteja correto." + }, + "step": { + "init": { + "description": "Para ativar a autentica\u00e7\u00e3o de dois fatores usando senhas de uso \u00fanico com base em tempo, digitalize o c\u00f3digo QR com seu aplicativo de autentica\u00e7\u00e3o. Se voc\u00ea n\u00e3o tiver um, recomendamos o [Google Authenticator] (https://support.google.com/accounts/answer/1066447) ou [Authy] (https://authy.com/). \n\n {qr_code} \n \n Depois de digitalizar o c\u00f3digo, insira o c\u00f3digo de seis d\u00edgitos do aplicativo para verificar a configura\u00e7\u00e3o. Se voc\u00ea tiver problemas para escanear o c\u00f3digo QR, fa\u00e7a uma configura\u00e7\u00e3o manual com o c\u00f3digo ** ` {code} ` **.", + "title": "Configure a autentica\u00e7\u00e3o de dois fatores usando o TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/pt.json new file mode 100644 index 00000000000..e25fe3139a4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/pt.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Nenhum servi\u00e7o de notifica\u00e7\u00e3o dispon\u00edvel." + }, + "error": { + "invalid_code": "C\u00f3digo inv\u00e1lido, por favor tente novamente." + }, + "step": { + "init": { + "description": "Por favor, selecione um dos servi\u00e7os de notifica\u00e7\u00e3o:", + "title": "Configurar uma palavra-passe entregue pela componente de notifica\u00e7\u00e3o" + }, + "setup": { + "description": "Foi enviada uma palavra-passe atrav\u00e9s de **notify.{notify_service}**. Por favor, insira-a:", + "title": "Verificar a configura\u00e7\u00e3o" + } + }, + "title": "Notificar palavra-passe de uso \u00fanico" + }, + "totp": { + "error": { + "invalid_code": "C\u00f3digo inv\u00e1lido, por favor, tente novamente. Se receber este erro constantemente, por favor, certifique-se de que o rel\u00f3gio do sistema que hospeda o Home Assistant \u00e9 preciso." + }, + "step": { + "init": { + "description": "Para ativar a autentica\u00e7\u00e3o com dois fatores utilizando palavras-passe de uso \u00fanico (OTP), ler o c\u00f3digo QR com a sua aplica\u00e7\u00e3o de autentica\u00e7\u00e3o. Se n\u00e3o tiver uma, recomendamos [Google Authenticator](https://support.google.com/accounts/answer/1066447) ou [Authy](https://authy.com/).\n\n{qr_code}\n\nDepois de ler o c\u00f3digo, introduza o c\u00f3digo de seis d\u00edgitos fornecido pela sua aplica\u00e7\u00e3o para verificar a configura\u00e7\u00e3o. Se tiver problemas a ler o c\u00f3digo QR, fa\u00e7a uma configura\u00e7\u00e3o manual com o c\u00f3digo **`{code}`**.", + "title": "Configurar autentica\u00e7\u00e3o com dois fatores usando TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/ro.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/ro.json new file mode 100644 index 00000000000..19f9ec10c73 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/ro.json @@ -0,0 +1,34 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Nu sunt disponibile servicii de notificare." + }, + "error": { + "invalid_code": "Cod invalid, va rugam incercati din nou." + }, + "step": { + "init": { + "description": "Selecta\u021bi unul dintre serviciile de notificare:", + "title": "Configura\u021bi o parol\u0103 unic\u0103 livrat\u0103 de o component\u0103 de notificare" + }, + "setup": { + "description": "O parol\u0103 unic\u0103 a fost trimis\u0103 prin **notify.{notify_service}**. Introduce\u021bi parola mai jos:", + "title": "Verifica\u021bi configurarea" + } + }, + "title": "Notifica\u021bi o parol\u0103 unic\u0103" + }, + "totp": { + "error": { + "invalid_code": "Cod invalid, va rugam incercati din nou. Dac\u0103 primi\u021bi aceast\u0103 eroare \u00een mod consecvent, asigura\u021bi-v\u0103 c\u0103 ceasul sistemului dvs. Home Assistant este corect." + }, + "step": { + "init": { + "title": "Configura\u021bi autentificarea cu doi factori utiliz\u00e2nd TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/ru.json new file mode 100644 index 00000000000..5092e079250 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/ru.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "\u041d\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u0441\u043b\u0443\u0436\u0431 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439." + }, + "error": { + "invalid_code": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u0434, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443." + }, + "step": { + "init": { + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u043d\u0443 \u0438\u0437 \u0441\u043b\u0443\u0436\u0431 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439:", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0438 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439 \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439" + }, + "setup": { + "description": "\u041e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d \u0447\u0435\u0440\u0435\u0437 **notify.{notify_service}**. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0435\u0433\u043e \u043d\u0438\u0436\u0435:", + "title": "\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443" + } + }, + "title": "\u0414\u043e\u0441\u0442\u0430\u0432\u043a\u0430 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439" + }, + "totp": { + "error": { + "invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430. \u0415\u0441\u043b\u0438 \u0412\u044b \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0435 \u044d\u0442\u0443 \u043e\u0448\u0438\u0431\u043a\u0443, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0447\u0430\u0441\u044b \u0432 \u0412\u0430\u0448\u0435\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Home Assistant \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u044e\u0442 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0435 \u0432\u0440\u0435\u043c\u044f." + }, + "step": { + "init": { + "description": "\u0427\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0443\u044e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439, \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u043d\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043e\u0442\u0441\u043a\u0430\u043d\u0438\u0440\u0443\u0439\u0442\u0435 QR-\u043a\u043e\u0434 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043f\u043e\u0434\u043b\u0438\u043d\u043d\u043e\u0441\u0442\u0438. \u0415\u0441\u043b\u0438 \u0443 \u0412\u0430\u0441 \u0435\u0433\u043e \u043d\u0435\u0442, \u043c\u044b \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043b\u0438\u0431\u043e [Google Authenticator](https://support.google.com/accounts/answer/1066447), \u043b\u0438\u0431\u043e [Authy](https://authy.com/). \n\n {qr_code} \n \n\u041f\u043e\u0441\u043b\u0435 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f QR-\u043a\u043e\u0434\u0430 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0448\u0435\u0441\u0442\u0438\u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u043a\u043e\u0434 \u0438\u0437 \u0412\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u0415\u0441\u043b\u0438 \u0443 \u0412\u0430\u0441 \u0435\u0441\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441\u043e \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u043c QR-\u043a\u043e\u0434\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a\u043e\u0434\u0430 **`{code}`**.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/sl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/sl.json new file mode 100644 index 00000000000..f70bb81e700 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/sl.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Storitve obve\u0161\u010danja niso na voljo." + }, + "error": { + "invalid_code": "Neveljavna koda, poskusite znova." + }, + "step": { + "init": { + "description": "Izberite eno od storitev obve\u0161\u010danja:", + "title": "Nastavite enkratno geslo, ki ga dostavite z obvestilno komponento" + }, + "setup": { + "description": "Enkratno geslo je poslal **notify.{notify_service} **. Prosimo, vnesite ga spodaj:", + "title": "Preverite nastavitev" + } + }, + "title": "Obvesti Enkratno Geslo" + }, + "totp": { + "error": { + "invalid_code": "Neveljavna koda, prosimo, poskusite znova. \u010ce dobite to sporo\u010dilo ve\u010dkrat, prosimo poskrbite, da bo ura va\u0161ega Home Assistanta to\u010dna." + }, + "step": { + "init": { + "description": "\u010ce \u017eelite aktivirati preverjanje pristnosti dveh faktorjev z enkratnimi gesli, ki temeljijo na \u010dasu, skenirajte kodo QR s svojo aplikacijo za preverjanje pristnosti. \u010ce je nimate, priporo\u010damo bodisi [Google Authenticator] (https://support.google.com/accounts/answer/1066447) ali [Authy] (https://authy.com/). \n\n {qr_code} \n \n Po skeniranju kode vnesite \u0161estmestno kodo iz aplikacije, da preverite nastavitev. \u010ce imate te\u017eave pri skeniranju kode QR, naredite ro\u010dno nastavitev s kodo ** ` {code} ` **.", + "title": "Nastavite dvofaktorsko avtentifikacijo s pomo\u010djo TOTP-ja" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/sv.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/sv.json new file mode 100644 index 00000000000..9246a88c512 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/sv.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Inga tillg\u00e4ngliga meddelande tj\u00e4nster." + }, + "error": { + "invalid_code": "Ogiltig kod, var god f\u00f6rs\u00f6k igen." + }, + "step": { + "init": { + "description": "Var god v\u00e4lj en av notifieringstj\u00e4nsterna:", + "title": "Konfigurera ett eng\u00e5ngsl\u00f6senord levererat genom notifieringskomponenten" + }, + "setup": { + "description": "Ett eng\u00e5ngsl\u00f6senord har skickats av **notify.{notify_service}**. V\u00e4nligen ange det nedan:", + "title": "Verifiera inst\u00e4llningen" + } + }, + "title": "Meddela eng\u00e5ngsl\u00f6senord" + }, + "totp": { + "error": { + "invalid_code": "Ogiltig kod, f\u00f6rs\u00f6k igen. Om du flera g\u00e5nger i rad f\u00e5r detta fel, se till att klockan i din Home Assistant \u00e4r korrekt inst\u00e4lld." + }, + "step": { + "init": { + "description": "F\u00f6r att aktivera tv\u00e5faktorsautentisering som anv\u00e4nder tidsbaserade eng\u00e5ngsl\u00f6senord, skanna QR-koden med din autentiseringsapp. Om du inte har en, rekommenderar vi antingen [Google Authenticator] (https://support.google.com/accounts/answer/1066447) eller [Authy] (https://authy.com/). \n\n{qr_code} \n\nN\u00e4r du har skannat koden anger du den sexsiffriga koden fr\u00e5n din app f\u00f6r att verifiera inst\u00e4llningen. Om du har problem med att skanna QR-koden, g\u00f6r en manuell inst\u00e4llning med kod ** ` {code} ` **.", + "title": "St\u00e4ll in tv\u00e5faktorsautentisering med TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/th.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/th.json new file mode 100644 index 00000000000..735b7e2fad5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/th.json @@ -0,0 +1,11 @@ +{ + "mfa_setup": { + "notify": { + "step": { + "setup": { + "title": "\u0e15\u0e23\u0e27\u0e08\u0e2a\u0e2d\u0e1a\u0e01\u0e32\u0e23\u0e15\u0e34\u0e14\u0e15\u0e31\u0e49\u0e07" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/tr.json new file mode 100644 index 00000000000..7d273214574 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/tr.json @@ -0,0 +1,22 @@ +{ + "mfa_setup": { + "notify": { + "step": { + "init": { + "title": "Bilgilendirme bile\u015feni taraf\u0131ndan verilen tek seferlik parolay\u0131 ayarlay\u0131n" + }, + "setup": { + "description": "**bildirim yoluyla tek seferlik bir parola g\u00f6nderildi. {notify_service}**. L\u00fctfen a\u015fa\u011f\u0131da girin:" + } + }, + "title": "Tek Seferlik Parolay\u0131 Bildir" + }, + "totp": { + "step": { + "init": { + "description": "Zamana dayal\u0131 tek seferlik parolalar\u0131 kullanarak iki fakt\u00f6rl\u00fc kimlik do\u011frulamay\u0131 etkinle\u015ftirmek i\u00e7in kimlik do\u011frulama uygulaman\u0131zla QR kodunu taray\u0131n. Hesab\u0131n\u0131z yoksa, [Google Authenticator] (https://support.google.com/accounts/answer/1066447) veya [Authy] (https://authy.com/) \u00f6neririz. \n\n {qr_code}\n\n Kodu tarad\u0131ktan sonra, kurulumu do\u011frulamak i\u00e7in uygulaman\u0131zdan alt\u0131 haneli kodu girin. QR kodunu taramayla ilgili sorun ya\u015f\u0131yorsan\u0131z, ** ` {code} ` manuel kurulum yap\u0131n." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/uk.json new file mode 100644 index 00000000000..eeb8f1ee7c7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/uk.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "\u041d\u0435\u043c\u0430\u0454 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u0441\u043b\u0443\u0436\u0431 \u0441\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u044c." + }, + "error": { + "invalid_code": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043e\u0434, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437." + }, + "step": { + "init": { + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u0434\u043d\u0443 \u0456\u0437 \u0441\u043b\u0443\u0436\u0431 \u0441\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u044c:", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0438 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u0438\u0445 \u043f\u0430\u0440\u043e\u043b\u0456\u0432 \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0441\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u044c" + }, + "setup": { + "description": "\u041e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u0438\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0439 \u0447\u0435\u0440\u0435\u0437 ** notify.{notify_service} **. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u0439\u043e\u0433\u043e \u043d\u0438\u0436\u0447\u0435:", + "title": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f" + } + }, + "title": "\u0414\u043e\u0441\u0442\u0430\u0432\u043a\u0430 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u0438\u0445 \u043f\u0430\u0440\u043e\u043b\u0456\u0432" + }, + "totp": { + "error": { + "invalid_code": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043e\u0434. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443. \u042f\u043a\u0449\u043e \u0412\u0438 \u043f\u043e\u0441\u0442\u0456\u0439\u043d\u043e \u043e\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u0435 \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443, \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0433\u043e\u0434\u0438\u043d\u043d\u0438\u043a \u0443 \u0412\u0430\u0448\u0456\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0456 Home Assistant \u043f\u043e\u043a\u0430\u0437\u0443\u0454 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u0447\u0430\u0441." + }, + "step": { + "init": { + "description": "\u0429\u043e\u0431 \u0430\u043a\u0442\u0438\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0443 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0437 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f\u043c \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u0438\u0445 \u043f\u0430\u0440\u043e\u043b\u0456\u0432, \u0437\u0430\u0441\u043d\u043e\u0432\u0430\u043d\u0438\u0445 \u043d\u0430 \u0447\u0430\u0441\u0456, \u0432\u0456\u0434\u0441\u043a\u0430\u043d\u0443\u0439\u0442\u0435 QR-\u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0438 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0438 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0447\u043d\u043e\u0441\u0442\u0456. \u042f\u043a\u0449\u043e \u0443 \u0412\u0430\u0441 \u0457\u0457 \u043d\u0435\u043c\u0430\u0454, \u043c\u0438 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0454\u043c\u043e \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0430\u0431\u043e [Google Authenticator](https://support.google.com/accounts/answer/1066447), \u0430\u0431\u043e [Authy](https://authy.com/). \n\n{qr_code}\n\n\u041f\u0456\u0441\u043b\u044f \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f QR-\u043a\u043e\u0434\u0443 \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u0448\u0435\u0441\u0442\u0438\u0437\u043d\u0430\u0447\u043d\u0438\u0439 \u043a\u043e\u0434 \u0437 \u0412\u0430\u0448\u043e\u0433\u043e \u0437\u0430\u0441\u0442\u043e\u0441\u0443\u0432\u0430\u043d\u043d\u044f, \u0449\u043e\u0431 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u042f\u043a\u0449\u043e \u0443 \u0412\u0430\u0441 \u0454 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0437\u0456 \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f\u043c QR-\u043a\u043e\u0434\u0443, \u0432\u0438\u043a\u043e\u043d\u0430\u0439\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u043a\u043e\u0434\u0443 ** `{code}` **.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457 \u0437 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f\u043c TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/vi.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/vi.json new file mode 100644 index 00000000000..02ac69bb983 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/vi.json @@ -0,0 +1,16 @@ +{ + "mfa_setup": { + "totp": { + "error": { + "invalid_code": "M\u00e3 kh\u00f4ng h\u1ee3p l\u1ec7, vui l\u00f2ng th\u1eed l\u1ea1i. N\u1ebfu b\u1ea1n g\u1eb7p l\u1ed7i n\u00e0y m\u1ed9t c\u00e1ch nh\u1ea5t qu\u00e1n, vui l\u00f2ng \u0111\u1ea3m b\u1ea3o \u0111\u1ed3ng h\u1ed3 c\u1ee7a h\u1ec7 th\u1ed1ng Home Assistant l\u00e0 ch\u00ednh x\u00e1c." + }, + "step": { + "init": { + "description": "\u0110\u1ec3 k\u00edch ho\u1ea1t x\u00e1c th\u1ef1c hai y\u1ebfu t\u1ed1 b\u1eb1ng m\u1eadt kh\u1ea9u m\u1ed9t l\u1ea7n d\u1ef1a tr\u00ean th\u1eddi gian, h\u00e3y qu\u00e9t m\u00e3 QR b\u1eb1ng \u1ee9ng d\u1ee5ng x\u00e1c th\u1ef1c c\u1ee7a b\u1ea1n. N\u1ebfu b\u1ea1n kh\u00f4ng c\u00f3, ch\u00fang t\u00f4i khuy\u00ean b\u1ea1n n\u00ean d\u00f9ng [Google Authenticator] (https://support.google.com/accounts/answer/1066447) ho\u1eb7c [Authy] (https://authy.com/). \n\n {qr_code} \n \n Sau khi qu\u00e9t m\u00e3, nh\u1eadp m\u00e3 s\u00e1u ch\u1eef s\u1ed1 t\u1eeb \u1ee9ng d\u1ee5ng c\u1ee7a b\u1ea1n \u0111\u1ec3 x\u00e1c minh thi\u1ebft l\u1eadp. N\u1ebfu b\u1ea1n g\u1eb7p v\u1ea5n \u0111\u1ec1 khi qu\u00e9t m\u00e3 QR, h\u00e3y th\u1ef1c hi\u1ec7n c\u00e0i \u0111\u1eb7t th\u1ee7 c\u00f4ng v\u1edbi m\u00e3 ** ` {code} ` **.", + "title": "Thi\u1ebft l\u1eadp x\u00e1c th\u1ef1c hai y\u1ebfu t\u1ed1 b\u1eb1ng TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/zh-Hans.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/zh-Hans.json new file mode 100644 index 00000000000..1cb311f016f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/zh-Hans.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "\u6ca1\u6709\u53ef\u7528\u7684\u901a\u77e5\u670d\u52a1\u3002" + }, + "error": { + "invalid_code": "\u4ee3\u7801\u65e0\u6548\uff0c\u8bf7\u518d\u8bd5\u4e00\u6b21\u3002" + }, + "step": { + "init": { + "description": "\u8bf7\u4ece\u4e0b\u9762\u9009\u62e9\u4e00\u4e2a\u901a\u77e5\u670d\u52a1\uff1a", + "title": "\u8bbe\u7f6e\u7531\u901a\u77e5\u7ec4\u4ef6\u4f20\u9012\u7684\u4e00\u6b21\u6027\u5bc6\u7801" + }, + "setup": { + "description": "\u4e00\u6b21\u6027\u5bc6\u7801\u5df2\u7531 **notify.{notify_service}** \u53d1\u9001\u3002\u8bf7\u5728\u4e0b\u9762\u8f93\u5165\uff1a", + "title": "\u9a8c\u8bc1\u8bbe\u7f6e" + } + }, + "title": "\u4e00\u6b21\u6027\u5bc6\u7801\u901a\u77e5" + }, + "totp": { + "error": { + "invalid_code": "\u53e3\u4ee4\u65e0\u6548\uff0c\u8bf7\u91cd\u65b0\u8f93\u5165\u3002\u5982\u679c\u9519\u8bef\u53cd\u590d\u51fa\u73b0\uff0c\u8bf7\u786e\u4fdd Home Assistant \u7cfb\u7edf\u7684\u65f6\u95f4\u51c6\u786e\u65e0\u8bef\u3002" + }, + "step": { + "init": { + "description": "\u8981\u6fc0\u6d3b\u57fa\u4e8e\u65f6\u95f4\u52a8\u6001\u53e3\u4ee4\u7684\u53cc\u91cd\u8ba4\u8bc1\uff0c\u8bf7\u7528\u8eab\u4efd\u9a8c\u8bc1\u5e94\u7528\u626b\u63cf\u4ee5\u4e0b\u4e8c\u7ef4\u7801\u3002\u5982\u679c\u60a8\u8fd8\u6ca1\u6709\u8eab\u4efd\u9a8c\u8bc1\u5e94\u7528\uff0c\u63a8\u8350\u4f7f\u7528 [Google \u8eab\u4efd\u9a8c\u8bc1\u5668](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u626b\u63cf\u4e8c\u7ef4\u7801\u4ee5\u540e\uff0c\u8f93\u5165\u5e94\u7528\u4e0a\u7684\u516d\u4f4d\u6570\u5b57\u53e3\u4ee4\u6765\u9a8c\u8bc1\u914d\u7f6e\u3002\u5982\u679c\u5728\u626b\u63cf\u4e8c\u7ef4\u7801\u65f6\u9047\u5230\u95ee\u9898\uff0c\u8bf7\u4f7f\u7528\u4ee3\u7801 **`{code}`** \u624b\u52a8\u914d\u7f6e\u3002", + "title": "\u7528\u65f6\u95f4\u52a8\u6001\u53e3\u4ee4\u8bbe\u7f6e\u53cc\u91cd\u8ba4\u8bc1" + } + }, + "title": "\u65f6\u95f4\u52a8\u6001\u53e3\u4ee4" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/zh-Hant.json new file mode 100644 index 00000000000..8e769cb5983 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/auth/translations/zh-Hant.json @@ -0,0 +1,35 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "\u6c92\u6709\u53ef\u7528\u7684\u901a\u77e5\u670d\u52d9\u3002" + }, + "error": { + "invalid_code": "\u9a57\u8b49\u78bc\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002" + }, + "step": { + "init": { + "description": "\u8acb\u9078\u64c7\u4e00\u9805\u901a\u77e5\u670d\u52d9\uff1a", + "title": "\u8a2d\u5b9a\u4e00\u6b21\u6027\u5bc6\u78bc\u50b3\u9001\u7d44\u4ef6" + }, + "setup": { + "description": "\u4e00\u6b21\u6027\u5bc6\u78bc\u5df2\u900f\u904e **notify.{notify_service}** \u50b3\u9001\u3002\u8acb\u65bc\u4e0b\u65b9\u8f38\u5165\uff1a", + "title": "\u9a57\u8b49\u8a2d\u5b9a" + } + }, + "title": "\u901a\u77e5\u4e00\u6b21\u6027\u5bc6\u78bc" + }, + "totp": { + "error": { + "invalid_code": "\u9a57\u8b49\u78bc\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002\u5047\u5982\u932f\u8aa4\u6301\u7e8c\u767c\u751f\uff0c\u8acb\u5148\u78ba\u5b9a\u60a8\u7684 Home Assistant \u7cfb\u7d71\u4e0a\u7684\u6642\u9593\u8a2d\u5b9a\u6b63\u78ba\u5f8c\uff0c\u518d\u8a66\u4e00\u6b21\u3002" + }, + "step": { + "init": { + "description": "\u6b32\u555f\u7528\u4e00\u6b21\u6027\u4e14\u5177\u6642\u6548\u6027\u7684\u5bc6\u78bc\u4e4b\u96d9\u91cd\u8a8d\u8b49\u529f\u80fd\uff0c\u8acb\u4f7f\u7528\u60a8\u7684\u9a57\u8b49 App \u6383\u7784\u4e0b\u65b9\u7684 QR code \u3002\u5018\u82e5\u60a8\u5c1a\u672a\u5b89\u88dd\u4efb\u4f55 App\uff0c\u63a8\u85a6\u60a8\u4f7f\u7528 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u65bc\u641c\u5c0b\u4e4b\u5f8c\uff0c\u8f38\u5165 App \u4e2d\u7684\u516d\u4f4d\u6578\u5b57\u9032\u884c\u8a2d\u5b9a\u9a57\u8b49\u3002\u5047\u5982\u641c\u5c0b\u51fa\u73fe\u554f\u984c\uff0c\u8acb\u624b\u52d5\u8f38\u5165\u4ee5\u4e0b\u9a57\u8b49\u78bc **`{code}`**\u3002", + "title": "\u4f7f\u7528 TOTP \u8a2d\u5b9a\u96d9\u91cd\u8a8d\u8b49" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/__init__.py new file mode 100644 index 00000000000..a338f6cf161 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/__init__.py @@ -0,0 +1,766 @@ +"""Allow to set up simple automation rules via the config file.""" +from __future__ import annotations + +import logging +from typing import Any, Awaitable, Callable, Dict, cast + +import voluptuous as vol +from voluptuous.humanize import humanize_error + +from homeassistant.components import blueprint +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_MODE, + ATTR_NAME, + CONF_ALIAS, + CONF_CONDITION, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + CONF_ID, + CONF_MODE, + CONF_PLATFORM, + CONF_VARIABLES, + CONF_ZONE, + EVENT_HOMEASSISTANT_STARTED, + SERVICE_RELOAD, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) +from homeassistant.core import ( + Context, + CoreState, + HomeAssistant, + callback, + split_entity_id, +) +from homeassistant.exceptions import ( + ConditionError, + ConditionErrorContainer, + ConditionErrorIndex, + HomeAssistantError, +) +from homeassistant.helpers import condition, extract_domain_configs, template +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.script import ( + ATTR_CUR, + ATTR_MAX, + CONF_MAX, + CONF_MAX_EXCEEDED, + Script, +) +from homeassistant.helpers.script_variables import ScriptVariables +from homeassistant.helpers.service import async_register_admin_service +from homeassistant.helpers.trace import ( + TraceElement, + script_execution_set, + trace_append_element, + trace_get, + trace_path, +) +from homeassistant.helpers.trigger import async_initialize_triggers +from homeassistant.helpers.typing import TemplateVarsType +from homeassistant.loader import bind_hass +from homeassistant.util.dt import parse_datetime + +from .config import AutomationConfig, async_validate_config_item + +# Not used except by packages to check config structure +from .config import PLATFORM_SCHEMA # noqa: F401 +from .const import ( + CONF_ACTION, + CONF_INITIAL_STATE, + CONF_TRACE, + CONF_TRIGGER, + CONF_TRIGGER_VARIABLES, + DEFAULT_INITIAL_STATE, + DOMAIN, + LOGGER, +) +from .helpers import async_get_blueprints +from .trace import trace_automation + +# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: no-check-untyped-defs, no-warn-return-any + +ENTITY_ID_FORMAT = DOMAIN + ".{}" + + +CONF_SKIP_CONDITION = "skip_condition" +CONF_STOP_ACTIONS = "stop_actions" +DEFAULT_STOP_ACTIONS = True + +EVENT_AUTOMATION_RELOADED = "automation_reloaded" +EVENT_AUTOMATION_TRIGGERED = "automation_triggered" + +ATTR_LAST_TRIGGERED = "last_triggered" +ATTR_SOURCE = "source" +ATTR_VARIABLES = "variables" +SERVICE_TRIGGER = "trigger" + +_LOGGER = logging.getLogger(__name__) +AutomationActionType = Callable[[HomeAssistant, TemplateVarsType], Awaitable[None]] + + +@bind_hass +def is_on(hass, entity_id): + """ + Return true if specified automation entity_id is on. + + Async friendly. + """ + return hass.states.is_state(entity_id, STATE_ON) + + +@callback +def automations_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: + """Return all automations that reference the entity.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + return [ + automation_entity.entity_id + for automation_entity in component.entities + if entity_id in automation_entity.referenced_entities + ] + + +@callback +def entities_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: + """Return all entities in a scene.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + automation_entity = component.get_entity(entity_id) + + if automation_entity is None: + return [] + + return list(automation_entity.referenced_entities) + + +@callback +def automations_with_device(hass: HomeAssistant, device_id: str) -> list[str]: + """Return all automations that reference the device.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + return [ + automation_entity.entity_id + for automation_entity in component.entities + if device_id in automation_entity.referenced_devices + ] + + +@callback +def devices_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: + """Return all devices in a scene.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + automation_entity = component.get_entity(entity_id) + + if automation_entity is None: + return [] + + return list(automation_entity.referenced_devices) + + +@callback +def automations_with_area(hass: HomeAssistant, area_id: str) -> list[str]: + """Return all automations that reference the area.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + return [ + automation_entity.entity_id + for automation_entity in component.entities + if area_id in automation_entity.referenced_areas + ] + + +@callback +def areas_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: + """Return all areas in an automation.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + automation_entity = component.get_entity(entity_id) + + if automation_entity is None: + return [] + + return list(automation_entity.referenced_areas) + + +async def async_setup(hass, config): + """Set up all automations.""" + # Local import to avoid circular import + hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass) + + # To register the automation blueprints + async_get_blueprints(hass) + + if not await _async_process_config(hass, config, component): + await async_get_blueprints(hass).async_populate() + + async def trigger_service_handler(entity, service_call): + """Handle forced automation trigger, e.g. from frontend.""" + await entity.async_trigger( + {**service_call.data[ATTR_VARIABLES], "trigger": {"platform": None}}, + skip_condition=service_call.data[CONF_SKIP_CONDITION], + context=service_call.context, + ) + + component.async_register_entity_service( + SERVICE_TRIGGER, + { + vol.Optional(ATTR_VARIABLES, default={}): dict, + vol.Optional(CONF_SKIP_CONDITION, default=True): bool, + }, + trigger_service_handler, + ) + component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") + component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") + component.async_register_entity_service( + SERVICE_TURN_OFF, + {vol.Optional(CONF_STOP_ACTIONS, default=DEFAULT_STOP_ACTIONS): cv.boolean}, + "async_turn_off", + ) + + async def reload_service_handler(service_call): + """Remove all automations and load new ones from config.""" + conf = await component.async_prepare_reload() + if conf is None: + return + async_get_blueprints(hass).async_reset_cache() + await _async_process_config(hass, conf, component) + hass.bus.async_fire(EVENT_AUTOMATION_RELOADED, context=service_call.context) + + async_register_admin_service( + hass, DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=vol.Schema({}) + ) + + return True + + +class AutomationEntity(ToggleEntity, RestoreEntity): + """Entity to show status of entity.""" + + def __init__( + self, + automation_id, + name, + trigger_config, + cond_func, + action_script, + initial_state, + variables, + trigger_variables, + raw_config, + blueprint_inputs, + trace_config, + ): + """Initialize an automation entity.""" + self._id = automation_id + self._name = name + self._trigger_config = trigger_config + self._async_detach_triggers = None + self._cond_func = cond_func + self.action_script = action_script + self.action_script.change_listener = self.async_write_ha_state + self._initial_state = initial_state + self._is_enabled = False + self._referenced_entities: set[str] | None = None + self._referenced_devices: set[str] | None = None + self._logger = LOGGER + self._variables: ScriptVariables = variables + self._trigger_variables: ScriptVariables = trigger_variables + self._raw_config = raw_config + self._blueprint_inputs = blueprint_inputs + self._trace_config = trace_config + + @property + def name(self): + """Name of the automation.""" + return self._name + + @property + def unique_id(self): + """Return unique ID.""" + return self._id + + @property + def should_poll(self): + """No polling needed for automation entities.""" + return False + + @property + def extra_state_attributes(self): + """Return the entity state attributes.""" + attrs = { + ATTR_LAST_TRIGGERED: self.action_script.last_triggered, + ATTR_MODE: self.action_script.script_mode, + ATTR_CUR: self.action_script.runs, + } + if self.action_script.supports_max: + attrs[ATTR_MAX] = self.action_script.max_runs + if self._id is not None: + attrs[CONF_ID] = self._id + return attrs + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self._async_detach_triggers is not None or self._is_enabled + + @property + def referenced_areas(self): + """Return a set of referenced areas.""" + return self.action_script.referenced_areas + + @property + def referenced_devices(self): + """Return a set of referenced devices.""" + if self._referenced_devices is not None: + return self._referenced_devices + + referenced = self.action_script.referenced_devices + + if self._cond_func is not None: + for conf in self._cond_func.config: + referenced |= condition.async_extract_devices(conf) + + for conf in self._trigger_config: + device = _trigger_extract_device(conf) + if device is not None: + referenced.add(device) + + self._referenced_devices = referenced + return referenced + + @property + def referenced_entities(self): + """Return a set of referenced entities.""" + if self._referenced_entities is not None: + return self._referenced_entities + + referenced = self.action_script.referenced_entities + + if self._cond_func is not None: + for conf in self._cond_func.config: + referenced |= condition.async_extract_entities(conf) + + for conf in self._trigger_config: + for entity_id in _trigger_extract_entities(conf): + referenced.add(entity_id) + + self._referenced_entities = referenced + return referenced + + async def async_added_to_hass(self) -> None: + """Startup with initial state or previous state.""" + await super().async_added_to_hass() + + self._logger = logging.getLogger( + f"{__name__}.{split_entity_id(self.entity_id)[1]}" + ) + self.action_script.update_logger(self._logger) + + state = await self.async_get_last_state() + if state: + enable_automation = state.state == STATE_ON + last_triggered = state.attributes.get("last_triggered") + if last_triggered is not None: + self.action_script.last_triggered = parse_datetime(last_triggered) + self._logger.debug( + "Loaded automation %s with state %s from state " + " storage last state %s", + self.entity_id, + enable_automation, + state, + ) + else: + enable_automation = DEFAULT_INITIAL_STATE + self._logger.debug( + "Automation %s not in state storage, state %s from default is used", + self.entity_id, + enable_automation, + ) + + if self._initial_state is not None: + enable_automation = self._initial_state + self._logger.debug( + "Automation %s initial state %s overridden from " + "config initial_state", + self.entity_id, + enable_automation, + ) + + if enable_automation: + await self.async_enable() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on and update the state.""" + await self.async_enable() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + if CONF_STOP_ACTIONS in kwargs: + await self.async_disable(kwargs[CONF_STOP_ACTIONS]) + else: + await self.async_disable() + + async def async_trigger(self, run_variables, context=None, skip_condition=False): + """Trigger automation. + + This method is a coroutine. + """ + reason = "" + if "trigger" in run_variables and "description" in run_variables["trigger"]: + reason = f' by {run_variables["trigger"]["description"]}' + self._logger.debug("Automation triggered%s", reason) + + # Create a new context referring to the old context. + parent_id = None if context is None else context.id + trigger_context = Context(parent_id=parent_id) + + with trace_automation( + self.hass, + self.unique_id, + self._raw_config, + self._blueprint_inputs, + trigger_context, + self._trace_config, + ) as automation_trace: + if self._variables: + try: + variables = self._variables.async_render(self.hass, run_variables) + except template.TemplateError as err: + self._logger.error("Error rendering variables: %s", err) + automation_trace.set_error(err) + return + else: + variables = run_variables + # Prepare tracing the automation + automation_trace.set_trace(trace_get()) + + # Set trigger reason + trigger_description = variables.get("trigger", {}).get("description") + automation_trace.set_trigger_description(trigger_description) + + # Add initial variables as the trigger step + if "trigger" in variables and "id" in variables["trigger"]: + trigger_path = f"trigger/{variables['trigger']['id']}" + else: + trigger_path = "trigger" + trace_element = TraceElement(variables, trigger_path) + trace_append_element(trace_element) + + if ( + not skip_condition + and self._cond_func is not None + and not self._cond_func(variables) + ): + self._logger.debug( + "Conditions not met, aborting automation. Condition summary: %s", + trace_get(clear=False), + ) + script_execution_set("failed_conditions") + return + + self.async_set_context(trigger_context) + event_data = { + ATTR_NAME: self._name, + ATTR_ENTITY_ID: self.entity_id, + } + if "trigger" in variables and "description" in variables["trigger"]: + event_data[ATTR_SOURCE] = variables["trigger"]["description"] + + @callback + def started_action(): + self.hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, event_data, context=trigger_context + ) + + try: + with trace_path("action"): + await self.action_script.async_run( + variables, trigger_context, started_action + ) + except (vol.Invalid, HomeAssistantError) as err: + self._logger.error( + "Error while executing automation %s: %s", + self.entity_id, + err, + ) + automation_trace.set_error(err) + except Exception as err: # pylint: disable=broad-except + self._logger.exception("While executing automation %s", self.entity_id) + automation_trace.set_error(err) + + async def async_will_remove_from_hass(self): + """Remove listeners when removing automation from Home Assistant.""" + await super().async_will_remove_from_hass() + await self.async_disable() + + async def async_enable(self): + """Enable this automation entity. + + This method is a coroutine. + """ + if self._is_enabled: + return + + self._is_enabled = True + + # HomeAssistant is starting up + if self.hass.state != CoreState.not_running: + self._async_detach_triggers = await self._async_attach_triggers(False) + self.async_write_ha_state() + return + + async def async_enable_automation(event): + """Start automation on startup.""" + # Don't do anything if no longer enabled or already attached + if not self._is_enabled or self._async_detach_triggers is not None: + return + + self._async_detach_triggers = await self._async_attach_triggers(True) + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, async_enable_automation + ) + self.async_write_ha_state() + + async def async_disable(self, stop_actions=DEFAULT_STOP_ACTIONS): + """Disable the automation entity.""" + if not self._is_enabled and not self.action_script.runs: + return + + self._is_enabled = False + + if self._async_detach_triggers is not None: + self._async_detach_triggers() + self._async_detach_triggers = None + + if stop_actions: + await self.action_script.async_stop() + + self.async_write_ha_state() + + async def _async_attach_triggers( + self, home_assistant_start: bool + ) -> Callable[[], None] | None: + """Set up the triggers.""" + + def log_cb(level, msg, **kwargs): + self._logger.log(level, "%s %s", msg, self._name, **kwargs) + + variables = None + if self._trigger_variables: + try: + variables = self._trigger_variables.async_render( + self.hass, None, limited=True + ) + except template.TemplateError as err: + self._logger.error("Error rendering trigger variables: %s", err) + return None + + return await async_initialize_triggers( + self.hass, + self._trigger_config, + self.async_trigger, + DOMAIN, + self._name, + log_cb, + home_assistant_start, + variables, + ) + + +async def _async_process_config( + hass: HomeAssistant, + config: dict[str, Any], + component: EntityComponent, +) -> bool: + """Process config and add automations. + + Returns if blueprints were used. + """ + entities = [] + blueprints_used = False + + for config_key in extract_domain_configs(config, DOMAIN): + conf: list[dict[str, Any] | blueprint.BlueprintInputs] = config[config_key] + + for list_no, config_block in enumerate(conf): + raw_blueprint_inputs = None + raw_config = None + if isinstance(config_block, blueprint.BlueprintInputs): + blueprints_used = True + blueprint_inputs = config_block + raw_blueprint_inputs = blueprint_inputs.config_with_inputs + + try: + raw_config = blueprint_inputs.async_substitute() + config_block = cast( + Dict[str, Any], + await async_validate_config_item(hass, raw_config), + ) + except vol.Invalid as err: + LOGGER.error( + "Blueprint %s generated invalid automation with inputs %s: %s", + blueprint_inputs.blueprint.name, + blueprint_inputs.inputs, + humanize_error(config_block, err), + ) + continue + else: + raw_config = cast(AutomationConfig, config_block).raw_config + + automation_id = config_block.get(CONF_ID) + name = config_block.get(CONF_ALIAS) or f"{config_key} {list_no}" + + initial_state = config_block.get(CONF_INITIAL_STATE) + + action_script = Script( + hass, + config_block[CONF_ACTION], + name, + DOMAIN, + running_description="automation actions", + script_mode=config_block[CONF_MODE], + max_runs=config_block[CONF_MAX], + max_exceeded=config_block[CONF_MAX_EXCEEDED], + logger=LOGGER, + # We don't pass variables here + # Automation will already render them to use them in the condition + # and so will pass them on to the script. + ) + + if CONF_CONDITION in config_block: + cond_func = await _async_process_if(hass, name, config, config_block) + + if cond_func is None: + continue + else: + cond_func = None + + # Add trigger variables to variables + variables = None + if CONF_TRIGGER_VARIABLES in config_block: + variables = ScriptVariables( + dict(config_block[CONF_TRIGGER_VARIABLES].as_dict()) + ) + if CONF_VARIABLES in config_block: + if variables: + variables.variables.update(config_block[CONF_VARIABLES].as_dict()) + else: + variables = config_block[CONF_VARIABLES] + + entity = AutomationEntity( + automation_id, + name, + config_block[CONF_TRIGGER], + cond_func, + action_script, + initial_state, + variables, + config_block.get(CONF_TRIGGER_VARIABLES), + raw_config, + raw_blueprint_inputs, + config_block[CONF_TRACE], + ) + + entities.append(entity) + + if entities: + await component.async_add_entities(entities) + + return blueprints_used + + +async def _async_process_if(hass, name, config, p_config): + """Process if checks.""" + if_configs = p_config[CONF_CONDITION] + + checks = [] + for if_config in if_configs: + try: + checks.append(await condition.async_from_config(hass, if_config, False)) + except HomeAssistantError as ex: + LOGGER.warning("Invalid condition: %s", ex) + return None + + def if_action(variables=None): + """AND all conditions.""" + errors = [] + for index, check in enumerate(checks): + try: + with trace_path(["condition", str(index)]): + if not check(hass, variables): + return False + except ConditionError as ex: + errors.append( + ConditionErrorIndex( + "condition", index=index, total=len(checks), error=ex + ) + ) + + if errors: + LOGGER.warning( + "Error evaluating condition in '%s':\n%s", + name, + ConditionErrorContainer("condition", errors=errors), + ) + return False + + return True + + if_action.config = if_configs + + return if_action + + +@callback +def _trigger_extract_device(trigger_conf: dict) -> str | None: + """Extract devices from a trigger config.""" + if trigger_conf[CONF_PLATFORM] != "device": + return None + + return trigger_conf[CONF_DEVICE_ID] + + +@callback +def _trigger_extract_entities(trigger_conf: dict) -> list[str]: + """Extract entities from a trigger config.""" + if trigger_conf[CONF_PLATFORM] in ("state", "numeric_state"): + return trigger_conf[CONF_ENTITY_ID] + + if trigger_conf[CONF_PLATFORM] == "zone": + return trigger_conf[CONF_ENTITY_ID] + [trigger_conf[CONF_ZONE]] + + if trigger_conf[CONF_PLATFORM] == "geo_location": + return [trigger_conf[CONF_ZONE]] + + if trigger_conf[CONF_PLATFORM] == "sun": + return ["sun.sun"] + + return [] diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/blueprints/motion_light.yaml b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/blueprints/motion_light.yaml new file mode 100644 index 00000000000..54a4a4f0643 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/blueprints/motion_light.yaml @@ -0,0 +1,54 @@ +blueprint: + name: Motion-activated Light + description: Turn on a light when motion is detected. + domain: automation + source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/motion_light.yaml + input: + motion_entity: + name: Motion Sensor + selector: + entity: + domain: binary_sensor + device_class: motion + light_target: + name: Light + selector: + target: + entity: + domain: light + no_motion_wait: + name: Wait time + description: Time to leave the light on after last motion is detected. + default: 120 + selector: + number: + min: 0 + max: 3600 + unit_of_measurement: seconds + +# If motion is detected within the delay, +# we restart the script. +mode: restart +max_exceeded: silent + +trigger: + platform: state + entity_id: !input motion_entity + from: "off" + to: "on" + +action: + - alias: "Turn on the light" + service: light.turn_on + target: !input light_target + - alias: "Wait until there is no motion from device" + wait_for_trigger: + platform: state + entity_id: !input motion_entity + from: "on" + to: "off" + - alias: "Wait the number of seconds that has been set" + delay: !input no_motion_wait + - alias: "Turn off the light" + service: light.turn_off + target: !input light_target diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml new file mode 100644 index 00000000000..71abf8f865c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml @@ -0,0 +1,44 @@ +blueprint: + name: Zone Notification + description: Send a notification to a device when a person leaves a specific zone. + domain: automation + source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml + input: + person_entity: + name: Person + selector: + entity: + domain: person + zone_entity: + name: Zone + selector: + entity: + domain: zone + notify_device: + name: Device to notify + description: Device needs to run the official Home Assistant app to receive notifications. + selector: + device: + integration: mobile_app + +trigger: + platform: state + entity_id: !input person_entity + +variables: + zone_entity: !input zone_entity + # This is the state of the person when it's in this zone. + zone_state: "{{ states[zone_entity].name }}" + person_entity: !input person_entity + person_name: "{{ states[person_entity].name }}" + +condition: + condition: template + value_template: "{{ trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}" + +action: + - alias: "Notify that a person has left the zone" + domain: mobile_app + type: notify + device_id: !input notify_device + message: "{{ person_name }} has left {{ zone_state }}" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/config.py b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/config.py new file mode 100644 index 00000000000..e28fa5c477f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/config.py @@ -0,0 +1,141 @@ +"""Config validation helper for the automation integration.""" +import asyncio +from contextlib import suppress + +import voluptuous as vol + +from homeassistant.components import blueprint +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) +from homeassistant.components.trace import TRACE_CONFIG_SCHEMA +from homeassistant.config import async_log_exception, config_without_domain +from homeassistant.const import ( + CONF_ALIAS, + CONF_CONDITION, + CONF_DESCRIPTION, + CONF_ID, + CONF_VARIABLES, +) +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_per_platform, config_validation as cv, script +from homeassistant.helpers.condition import async_validate_condition_config +from homeassistant.helpers.trigger import async_validate_trigger_config +from homeassistant.loader import IntegrationNotFound + +from .const import ( + CONF_ACTION, + CONF_HIDE_ENTITY, + CONF_INITIAL_STATE, + CONF_TRACE, + CONF_TRIGGER, + CONF_TRIGGER_VARIABLES, + DOMAIN, +) +from .helpers import async_get_blueprints + +# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: no-check-untyped-defs, no-warn-return-any + +_CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA]) + +PLATFORM_SCHEMA = vol.All( + cv.deprecated(CONF_HIDE_ENTITY), + script.make_script_schema( + { + # str on purpose + CONF_ID: str, + CONF_ALIAS: cv.string, + vol.Optional(CONF_DESCRIPTION): cv.string, + vol.Optional(CONF_TRACE, default={}): TRACE_CONFIG_SCHEMA, + vol.Optional(CONF_INITIAL_STATE): cv.boolean, + vol.Optional(CONF_HIDE_ENTITY): cv.boolean, + vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA, + vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA, + vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA, + vol.Optional(CONF_TRIGGER_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA, + vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA, + }, + script.SCRIPT_MODE_SINGLE, + ), +) + + +async def async_validate_config_item(hass, config, full_config=None): + """Validate config item.""" + if blueprint.is_blueprint_instance_config(config): + blueprints = async_get_blueprints(hass) + return await blueprints.async_inputs_from_config(config) + + config = PLATFORM_SCHEMA(config) + + config[CONF_TRIGGER] = await async_validate_trigger_config( + hass, config[CONF_TRIGGER] + ) + + if CONF_CONDITION in config: + config[CONF_CONDITION] = await asyncio.gather( + *[ + async_validate_condition_config(hass, cond) + for cond in config[CONF_CONDITION] + ] + ) + + config[CONF_ACTION] = await script.async_validate_actions_config( + hass, config[CONF_ACTION] + ) + + return config + + +class AutomationConfig(dict): + """Dummy class to allow adding attributes.""" + + raw_config = None + + +async def _try_async_validate_config_item(hass, config, full_config=None): + """Validate config item.""" + raw_config = None + with suppress(ValueError): + raw_config = dict(config) + + try: + config = await async_validate_config_item(hass, config, full_config) + except ( + vol.Invalid, + HomeAssistantError, + IntegrationNotFound, + InvalidDeviceAutomationConfig, + ) as ex: + async_log_exception(ex, DOMAIN, full_config or config, hass) + return None + + if isinstance(config, blueprint.BlueprintInputs): + return config + + config = AutomationConfig(config) + config.raw_config = raw_config + return config + + +async def async_validate_config(hass, config): + """Validate config.""" + automations = list( + filter( + lambda x: x is not None, + await asyncio.gather( + *( + _try_async_validate_config_item(hass, p_config, config) + for _, p_config in config_per_platform(config, DOMAIN) + ) + ), + ) + ) + + # Create a copy of the configuration with all config for current + # component removed and add validated config back in. + config = config_without_domain(config, DOMAIN) + config[DOMAIN] = automations + + return config diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/const.py new file mode 100644 index 00000000000..a82c78ded77 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/const.py @@ -0,0 +1,19 @@ +"""Constants for the automation integration.""" +import logging + +CONF_ACTION = "action" +CONF_TRIGGER = "trigger" +CONF_TRIGGER_VARIABLES = "trigger_variables" +DOMAIN = "automation" + +CONF_HIDE_ENTITY = "hide_entity" + +CONF_CONDITION_TYPE = "condition_type" +CONF_INITIAL_STATE = "initial_state" +CONF_BLUEPRINT = "blueprint" +CONF_INPUT = "input" +CONF_TRACE = "trace" + +DEFAULT_INITIAL_STATE = True + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/helpers.py b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/helpers.py new file mode 100644 index 00000000000..3be11afe18b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/helpers.py @@ -0,0 +1,15 @@ +"""Helpers for automation integration.""" +from homeassistant.components import blueprint +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.singleton import singleton + +from .const import DOMAIN, LOGGER + +DATA_BLUEPRINTS = "automation_blueprints" + + +@singleton(DATA_BLUEPRINTS) +@callback +def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints: + """Get automation blueprints.""" + return blueprint.DomainBlueprints(hass, DOMAIN, LOGGER) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/logbook.py b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/logbook.py new file mode 100644 index 00000000000..b5dbada1b7a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/logbook.py @@ -0,0 +1,31 @@ +"""Describe logbook events.""" +from homeassistant.components.logbook import LazyEventPartialState +from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME +from homeassistant.core import HomeAssistant, callback + +from . import ATTR_SOURCE, DOMAIN, EVENT_AUTOMATION_TRIGGERED + + +@callback +def async_describe_events(hass: HomeAssistant, async_describe_event): # type: ignore + """Describe logbook events.""" + + @callback + def async_describe_logbook_event(event: LazyEventPartialState): # type: ignore + """Describe a logbook event.""" + data = event.data + message = "has been triggered" + if ATTR_SOURCE in data: + message = f"{message} by {data[ATTR_SOURCE]}" + + return { + "name": data.get(ATTR_NAME), + "message": message, + "source": data.get(ATTR_SOURCE), + "entity_id": data.get(ATTR_ENTITY_ID), + "context_id": event.context_id, + } + + async_describe_event( + DOMAIN, EVENT_AUTOMATION_TRIGGERED, async_describe_logbook_event + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/manifest.json new file mode 100644 index 00000000000..9dd0130ee2f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "automation", + "name": "Automation", + "documentation": "https://www.home-assistant.io/integrations/automation", + "dependencies": ["blueprint", "trace"], + "after_dependencies": ["device_automation", "webhook"], + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/reproduce_state.py b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/reproduce_state.py new file mode 100644 index 00000000000..dd2ba824f8a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/reproduce_state.py @@ -0,0 +1,76 @@ +"""Reproduce an Automation state.""" +from __future__ import annotations + +import asyncio +from collections.abc import Iterable +import logging +from typing import Any + +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import Context, HomeAssistant, State + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATE_ON, STATE_OFF} + + +async def _async_reproduce_state( + hass: HomeAssistant, + state: State, + *, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATE_ON: + service = SERVICE_TURN_ON + elif state.state == STATE_OFF: + service = SERVICE_TURN_OFF + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistant, + states: Iterable[State], + *, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, +) -> None: + """Reproduce Automation states.""" + await asyncio.gather( + *( + _async_reproduce_state( + hass, state, context=context, reproduce_options=reproduce_options + ) + for state in states + ) + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/services.yaml b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/services.yaml new file mode 100644 index 00000000000..dec5793d1e7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/services.yaml @@ -0,0 +1,48 @@ +# Describes the format for available automation services +turn_on: + name: Turn on + description: Enable an automation. + target: + entity: + domain: automation + +turn_off: + name: Turn off + description: Disable an automation. + target: + entity: + domain: automation + fields: + stop_actions: + name: Stop actions + description: Stop currently running actions. + default: true + example: true + selector: + boolean: + +toggle: + name: Toggle + description: Toggle (enable / disable) an automation. + target: + entity: + domain: automation + +trigger: + name: Trigger + description: Trigger the actions of an automation. + target: + entity: + domain: automation + fields: + skip_condition: + name: Skip conditions + description: Whether or not the conditions will be skipped. + default: true + example: true + selector: + boolean: + +reload: + name: Reload + description: Reload the automation configuration. diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/strings.json new file mode 100644 index 00000000000..adcc505b145 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/strings.json @@ -0,0 +1,9 @@ +{ + "title": "Automation", + "state": { + "_": { + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]" + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/trace.py b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/trace.py new file mode 100644 index 00000000000..102aeda5a65 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/trace.py @@ -0,0 +1,57 @@ +"""Trace support for automation.""" +from __future__ import annotations + +from contextlib import contextmanager +from typing import Any + +from homeassistant.components.trace import ActionTrace, async_store_trace +from homeassistant.components.trace.const import CONF_STORED_TRACES +from homeassistant.core import Context + +# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: no-check-untyped-defs, no-warn-return-any + + +class AutomationTrace(ActionTrace): + """Container for automation trace.""" + + def __init__( + self, + item_id: str, + config: dict[str, Any], + blueprint_inputs: dict[str, Any], + context: Context, + ): + """Container for automation trace.""" + key = ("automation", item_id) + super().__init__(key, config, blueprint_inputs, context) + self._trigger_description: str | None = None + + def set_trigger_description(self, trigger: str) -> None: + """Set trigger description.""" + self._trigger_description = trigger + + def as_short_dict(self) -> dict[str, Any]: + """Return a brief dictionary version of this AutomationTrace.""" + result = super().as_short_dict() + result["trigger"] = self._trigger_description + return result + + +@contextmanager +def trace_automation( + hass, automation_id, config, blueprint_inputs, context, trace_config +): + """Trace action execution of automation with automation_id.""" + trace = AutomationTrace(automation_id, config, blueprint_inputs, context) + async_store_trace(hass, trace, trace_config[CONF_STORED_TRACES]) + + try: + yield trace + except Exception as ex: + if automation_id: + trace.set_error(ex) + raise ex + finally: + if automation_id: + trace.finished() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/af.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/af.json new file mode 100644 index 00000000000..c821073c2ed --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/af.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Af", + "on": "Aan" + } + }, + "title": "Outomatisering" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ar.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ar.json new file mode 100644 index 00000000000..392afb2946f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ar.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "\u0625\u064a\u0642\u0627\u0641", + "on": "\u062a\u0634\u063a\u064a\u0644" + } + }, + "title": "\u0627\u0644\u062a\u0634\u063a\u064a\u0644 \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/bg.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/bg.json new file mode 100644 index 00000000000..1e294bff9a7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/bg.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d" + } + }, + "title": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/bs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/bs.json new file mode 100644 index 00000000000..c40d856e4bb --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/bs.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Isklju\u010den", + "on": "Uklju\u010den" + } + }, + "title": "Automatizacija" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ca.json new file mode 100644 index 00000000000..c1d35331e2b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ca.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "OFF", + "on": "ON" + } + }, + "title": "Automatitzaci\u00f3" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/cs.json new file mode 100644 index 00000000000..b4b3c61be5a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/cs.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Vypnuto", + "on": "Zapnuto" + } + }, + "title": "Automatizace" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/cy.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/cy.json new file mode 100644 index 00000000000..8239d527af3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/cy.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "I ffwrdd", + "on": "Ar" + } + }, + "title": "Awtomeiddio" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/da.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/da.json new file mode 100644 index 00000000000..755c3719ee8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/da.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Fra", + "on": "Til" + } + }, + "title": "Automatisering" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/de.json new file mode 100644 index 00000000000..9920c73d447 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/de.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Aus", + "on": "An" + } + }, + "title": "Automatisierung" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/el.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/el.json new file mode 100644 index 00000000000..14f41748830 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/el.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", + "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc\u03c2 " + } + }, + "title": "\u0391\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03cc\u03c2" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/en.json new file mode 100644 index 00000000000..e5dabcf3bce --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/en.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Off", + "on": "On" + } + }, + "title": "Automation" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/es-419.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/es-419.json new file mode 100644 index 00000000000..30b83fcacaf --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/es-419.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Desactivado", + "on": "Encendido" + } + }, + "title": "Automatizaci\u00f3n" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/es.json new file mode 100644 index 00000000000..c20f1be7d1d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/es.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Apagado", + "on": "Encendida" + } + }, + "title": "Automatizaci\u00f3n" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/et.json new file mode 100644 index 00000000000..71df51e9147 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/et.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "V\u00e4ljas", + "on": "Sees" + } + }, + "title": "Automatiseerimine" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/eu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/eu.json new file mode 100644 index 00000000000..e0c3e625dfd --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/eu.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Itzalita", + "on": "Piztuta" + } + }, + "title": "Automatizazioa" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/fa.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/fa.json new file mode 100644 index 00000000000..78b9a05540a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/fa.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "\u062e\u0627\u0645\u0648\u0634", + "on": "\u0641\u0639\u0627\u0644" + } + }, + "title": "\u0627\u062a\u0648\u0645\u0627\u0633\u06cc\u0648\u0646" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/fi.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/fi.json new file mode 100644 index 00000000000..b55e959d0c5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/fi.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Pois", + "on": "P\u00e4\u00e4ll\u00e4" + } + }, + "title": "Automaatio" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/fr.json new file mode 100644 index 00000000000..30426582414 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/fr.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Inactif", + "on": "Actif" + } + }, + "title": "Automatisation" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/gsw.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/gsw.json new file mode 100644 index 00000000000..4cdd801926a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/gsw.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Us", + "on": "Ah" + } + }, + "title": "Automation" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/he.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/he.json new file mode 100644 index 00000000000..6e4decfce9a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/he.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "\u05db\u05d1\u05d5\u05d9", + "on": "\u05d3\u05dc\u05d5\u05e7" + } + }, + "title": "\u05d0\u05d5\u05d8\u05d5\u05de\u05e6\u05d9\u05d4" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/hi.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/hi.json new file mode 100644 index 00000000000..d68188a8010 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/hi.json @@ -0,0 +1,8 @@ +{ + "state": { + "_": { + "off": "\u092c\u0902\u0926" + } + }, + "title": "\u0938\u094d\u0935\u091a\u093e\u0932\u0928" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/hr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/hr.json new file mode 100644 index 00000000000..c40d856e4bb --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/hr.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Isklju\u010den", + "on": "Uklju\u010den" + } + }, + "title": "Automatizacija" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/hu.json new file mode 100644 index 00000000000..85640af23ba --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/hu.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Ki", + "on": "Be" + } + }, + "title": "Automatiz\u00e1l\u00e1s" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/hy.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/hy.json new file mode 100644 index 00000000000..a421380748b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/hy.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "\u0531\u0576\u057b\u0561\u057f\u057e\u0561\u056e", + "on": "\u0544\u056b\u0561\u0581\u0561\u056e" + } + }, + "title": "\u0531\u057e\u057f\u0578\u0574\u0561\u057f\u0561\u0581\u0578\u0582\u0574" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/id.json new file mode 100644 index 00000000000..58e8497c8b9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/id.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Mati", + "on": "Nyala" + } + }, + "title": "Otomasi" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/is.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/is.json new file mode 100644 index 00000000000..7585e03c3b5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/is.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "\u00d3virk", + "on": "Virk" + } + }, + "title": "Sj\u00e1lfvirkni" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/it.json new file mode 100644 index 00000000000..c913ae7de4d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/it.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Spento", + "on": "Acceso" + } + }, + "title": "Automazione" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ja.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ja.json new file mode 100644 index 00000000000..ffd515979a2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ja.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "\u30aa\u30d5", + "on": "\u30aa\u30f3" + } + }, + "title": "\u30aa\u30fc\u30c8\u30e1\u30fc\u30b7\u30e7\u30f3" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ko.json new file mode 100644 index 00000000000..18be137be1b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ko.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "\uaebc\uc9d0", + "on": "\ucf1c\uc9d0" + } + }, + "title": "\uc790\ub3d9\ud654" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/lb.json new file mode 100644 index 00000000000..8a4ef4d9bf1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/lb.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Aus", + "on": "Un" + } + }, + "title": "Automatismen" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/lt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/lt.json new file mode 100644 index 00000000000..3cf0e9b442d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/lt.json @@ -0,0 +1,8 @@ +{ + "state": { + "_": { + "off": "I\u0161jungta", + "on": "\u012ejungta" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/lv.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/lv.json new file mode 100644 index 00000000000..48407ed6ab8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/lv.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Izsl\u0113gts", + "on": "Iesl\u0113gts" + } + }, + "title": "Automatiz\u0101cija" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/nb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/nb.json new file mode 100644 index 00000000000..64e00db42ca --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/nb.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Av", + "on": "P\u00e5" + } + }, + "title": "Automasjon" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/nl.json new file mode 100644 index 00000000000..7ef3acc9f2c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/nl.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Uit", + "on": "Aan" + } + }, + "title": "Automatisering" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/nn.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/nn.json new file mode 100644 index 00000000000..7c18b2e2ce2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/nn.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Av", + "on": "P\u00e5" + } + }, + "title": "Automasjonar" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/no.json new file mode 100644 index 00000000000..64e00db42ca --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/no.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Av", + "on": "P\u00e5" + } + }, + "title": "Automasjon" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/pl.json new file mode 100644 index 00000000000..959fb48b355 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/pl.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "wy\u0142.", + "on": "w\u0142." + } + }, + "title": "Automatyzacja" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/pt-BR.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/pt-BR.json new file mode 100644 index 00000000000..30c78d0a187 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/pt-BR.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Desligado", + "on": "Ativa" + } + }, + "title": "Automa\u00e7\u00e3o" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/pt.json new file mode 100644 index 00000000000..447658433e5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/pt.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Desligado", + "on": "Ligado" + } + }, + "title": "Automa\u00e7\u00e3o" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ro.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ro.json new file mode 100644 index 00000000000..f21db43282c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ro.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Oprit", + "on": "Pornit" + } + }, + "title": "Automatiz\u0103ri" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ru.json new file mode 100644 index 00000000000..d98f55a898e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ru.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + } + }, + "title": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/sk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/sk.json new file mode 100644 index 00000000000..a300acd23da --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/sk.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Neakt\u00edvny", + "on": "Akt\u00edvna" + } + }, + "title": "Automatiz\u00e1cia" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/sl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/sl.json new file mode 100644 index 00000000000..9045a3f3d36 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/sl.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Izklju\u010den", + "on": "Vklopljen" + } + }, + "title": "Avtomatizacija" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/sv.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/sv.json new file mode 100644 index 00000000000..8a5e2e58a9c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/sv.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Av", + "on": "P\u00e5" + } + }, + "title": "Automation" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ta.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ta.json new file mode 100644 index 00000000000..27ed507378f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/ta.json @@ -0,0 +1,8 @@ +{ + "state": { + "_": { + "off": "\u0b86\u0b83\u0baa\u0bcd", + "on": "\u0b86\u0ba9\u0bcd " + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/te.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/te.json new file mode 100644 index 00000000000..9577cca49cc --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/te.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "\u0c06\u0c2b\u0c4d", + "on": "\u0c06\u0c28\u0c4d" + } + }, + "title": "\u0c06\u0c1f\u0c4b\u0c2e\u0c47\u0c37\u0c28\u0c4d" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/th.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/th.json new file mode 100644 index 00000000000..0754717d6ab --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/th.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "\u0e1b\u0e34\u0e14", + "on": "\u0e40\u0e1b\u0e34\u0e14" + } + }, + "title": "\u0e01\u0e32\u0e23\u0e17\u0e33\u0e07\u0e32\u0e19\u0e2d\u0e31\u0e15\u0e42\u0e19\u0e21\u0e31\u0e15\u0e34" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/tr.json new file mode 100644 index 00000000000..804b616bfae --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/tr.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Kapal\u0131", + "on": "A\u00e7\u0131k" + } + }, + "title": "Otomasyon" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/uk.json new file mode 100644 index 00000000000..aa6eebb40c9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/uk.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e", + "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" + } + }, + "title": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0456\u044f" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/vi.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/vi.json new file mode 100644 index 00000000000..8b466688be9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/vi.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "T\u1eaft", + "on": "B\u1eadt" + } + }, + "title": "T\u1ef1 \u0111\u1ed9ng h\u00f3a" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/zh-Hans.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/zh-Hans.json new file mode 100644 index 00000000000..8a6cdbc5db8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/zh-Hans.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "\u5173\u95ed", + "on": "\u5f00\u542f" + } + }, + "title": "\u81ea\u52a8\u5316" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/zh-Hant.json new file mode 100644 index 00000000000..3fd099ef8d8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/automation/translations/zh-Hant.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "\u95dc\u9589", + "on": "\u958b\u555f" + } + }, + "title": "\u81ea\u52d5\u5316" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/avea/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/avea/__init__.py new file mode 100644 index 00000000000..861c4f655a1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/avea/__init__.py @@ -0,0 +1 @@ +"""The avea component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/avea/light.py b/homeassistant-2021.6.0.dev0/homeassistant/components/avea/light.py new file mode 100644 index 00000000000..eca020f6cd0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/avea/light.py @@ -0,0 +1,87 @@ +"""Support for the Elgato Avea lights.""" +import avea # pylint: disable=import-error + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + LightEntity, +) +from homeassistant.exceptions import PlatformNotReady +import homeassistant.util.color as color_util + +SUPPORT_AVEA = SUPPORT_BRIGHTNESS | SUPPORT_COLOR + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Avea platform.""" + try: + nearby_bulbs = avea.discover_avea_bulbs() + for bulb in nearby_bulbs: + bulb.get_name() + bulb.get_brightness() + except OSError as err: + raise PlatformNotReady from err + + add_entities(AveaLight(bulb) for bulb in nearby_bulbs) + + +class AveaLight(LightEntity): + """Representation of an Avea.""" + + def __init__(self, light): + """Initialize an AveaLight.""" + self._light = light + self._name = light.name + self._state = None + self._brightness = light.brightness + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_AVEA + + @property + def name(self): + """Return the display name of this light.""" + return self._name + + @property + def brightness(self): + """Return the brightness of the light.""" + return self._brightness + + @property + def is_on(self): + """Return true if light is on.""" + return self._state + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + if not kwargs: + self._light.set_brightness(4095) + else: + if ATTR_BRIGHTNESS in kwargs: + bright = round((kwargs[ATTR_BRIGHTNESS] / 255) * 4095) + self._light.set_brightness(bright) + if ATTR_HS_COLOR in kwargs: + rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) + self._light.set_rgb(rgb[0], rgb[1], rgb[2]) + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + self._light.set_brightness(0) + + def update(self): + """Fetch new state data for this light. + + This is the only method that should fetch new data for Home Assistant. + """ + brightness = self._light.get_brightness() + if brightness is not None: + if brightness == 0: + self._state = False + else: + self._state = True + self._brightness = round(255 * (brightness / 4095)) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/avea/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/avea/manifest.json new file mode 100644 index 00000000000..223ceba7685 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/avea/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "avea", + "name": "Elgato Avea", + "documentation": "https://www.home-assistant.io/integrations/avea", + "codeowners": ["@pattyland"], + "requirements": ["avea==1.5.1"], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/avion/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/avion/__init__.py new file mode 100644 index 00000000000..79e88222545 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/avion/__init__.py @@ -0,0 +1 @@ +"""The avion component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/avion/light.py b/homeassistant-2021.6.0.dev0/homeassistant/components/avion/light.py new file mode 100644 index 00000000000..0d242b952dd --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/avion/light.py @@ -0,0 +1,141 @@ +"""Support for Avion dimmers.""" +import importlib +import time + +import voluptuous as vol + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, + LightEntity, +) +from homeassistant.const import ( + CONF_API_KEY, + CONF_DEVICES, + CONF_ID, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, +) +import homeassistant.helpers.config_validation as cv + +SUPPORT_AVION_LED = SUPPORT_BRIGHTNESS + +DEVICE_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_ID): cv.positive_int, + vol.Optional(CONF_NAME): cv.string, + } +) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up an Avion switch.""" + avion = importlib.import_module("avion") + + lights = [] + if CONF_USERNAME in config and CONF_PASSWORD in config: + devices = avion.get_devices(config[CONF_USERNAME], config[CONF_PASSWORD]) + for device in devices: + lights.append(AvionLight(device)) + + for address, device_config in config[CONF_DEVICES].items(): + device = avion.Avion( + mac=address, + passphrase=device_config[CONF_API_KEY], + name=device_config.get(CONF_NAME), + object_id=device_config.get(CONF_ID), + connect=False, + ) + lights.append(AvionLight(device)) + + add_entities(lights) + + +class AvionLight(LightEntity): + """Representation of an Avion light.""" + + def __init__(self, device): + """Initialize the light.""" + self._name = device.name + self._address = device.mac + self._brightness = 255 + self._state = False + self._switch = device + + @property + def unique_id(self): + """Return the ID of this light.""" + return self._address + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return self._brightness + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_AVION_LED + + @property + def should_poll(self): + """Don't poll.""" + return False + + @property + def assumed_state(self): + """We can't read the actual state, so assume it matches.""" + return True + + def set_state(self, brightness): + """Set the state of this lamp to the provided brightness.""" + avion = importlib.import_module("avion") + + # Bluetooth LE is unreliable, and the connection may drop at any + # time. Make an effort to re-establish the link. + initial = time.monotonic() + while True: + if time.monotonic() - initial >= 10: + return False + try: + self._switch.set_brightness(brightness) + break + except avion.AvionException: + self._switch.connect() + return True + + def turn_on(self, **kwargs): + """Turn the specified or all lights on.""" + brightness = kwargs.get(ATTR_BRIGHTNESS) + + if brightness is not None: + self._brightness = brightness + + self.set_state(self.brightness) + self._state = True + + def turn_off(self, **kwargs): + """Turn the specified or all lights off.""" + self.set_state(0) + self._state = False diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/avion/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/avion/manifest.json new file mode 100644 index 00000000000..7ee6af89347 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/avion/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "avion", + "name": "Avi-on", + "documentation": "https://www.home-assistant.io/integrations/avion", + "requirements": ["avion==0.10"], + "codeowners": [], + "iot_class": "assumed_state" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/__init__.py new file mode 100644 index 00000000000..6af2850ea31 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/__init__.py @@ -0,0 +1,80 @@ +"""The awair component.""" +from __future__ import annotations + +from asyncio import gather +from typing import Any + +from async_timeout import timeout +from python_awair import Awair +from python_awair.exceptions import AuthError + +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import API_TIMEOUT, DOMAIN, LOGGER, UPDATE_INTERVAL, AwairResult + +PLATFORMS = ["sensor"] + + +async def async_setup_entry(hass, config_entry) -> bool: + """Set up Awair integration from a config entry.""" + session = async_get_clientsession(hass) + coordinator = AwairDataUpdateCoordinator(hass, config_entry, session) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][config_entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass, config_entry) -> bool: + """Unload Awair configuration.""" + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + + if unload_ok: + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok + + +class AwairDataUpdateCoordinator(DataUpdateCoordinator): + """Define a wrapper class to update Awair data.""" + + def __init__(self, hass, config_entry, session) -> None: + """Set up the AwairDataUpdateCoordinator class.""" + access_token = config_entry.data[CONF_ACCESS_TOKEN] + self._awair = Awair(access_token=access_token, session=session) + self._config_entry = config_entry + + super().__init__(hass, LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) + + async def _async_update_data(self) -> Any | None: + """Update data via Awair client library.""" + with timeout(API_TIMEOUT): + try: + LOGGER.debug("Fetching users and devices") + user = await self._awair.user() + devices = await user.devices() + results = await gather( + *[self._fetch_air_data(device) for device in devices] + ) + return {result.device.uuid: result for result in results} + except AuthError as err: + raise ConfigEntryAuthFailed from err + except Exception as err: + raise UpdateFailed(err) from err + + async def _fetch_air_data(self, device): + """Fetch latest air quality data.""" + LOGGER.debug("Fetching data for %s", device.uuid) + air_data = await device.air_data_latest() + LOGGER.debug(air_data) + return AwairResult(device=device, air_data=air_data) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/config_flow.py new file mode 100644 index 00000000000..2214fc30519 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/config_flow.py @@ -0,0 +1,103 @@ +"""Config flow for Awair.""" +from __future__ import annotations + +from python_awair import Awair +from python_awair.exceptions import AuthError, AwairError +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN, LOGGER + + +class AwairFlowHandler(ConfigFlow, domain=DOMAIN): + """Config flow for Awair.""" + + VERSION = 1 + + async def async_step_import(self, conf: dict): + """Import a configuration from config.yaml.""" + if self._async_current_entries(): + return self.async_abort(reason="already_setup") + + user, error = await self._check_connection(conf[CONF_ACCESS_TOKEN]) + if error is not None: + return self.async_abort(reason=error) + + await self.async_set_unique_id(user.email) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=f"{user.email} ({user.user_id})", + data={CONF_ACCESS_TOKEN: conf[CONF_ACCESS_TOKEN]}, + ) + + async def async_step_user(self, user_input: dict | None = None): + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + user, error = await self._check_connection(user_input[CONF_ACCESS_TOKEN]) + + if user is not None: + await self.async_set_unique_id(user.email) + self._abort_if_unique_id_configured() + + title = f"{user.email} ({user.user_id})" + return self.async_create_entry(title=title, data=user_input) + + if error != "invalid_access_token": + return self.async_abort(reason=error) + + errors = {CONF_ACCESS_TOKEN: "invalid_access_token"} + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}), + errors=errors, + ) + + async def async_step_reauth(self, user_input: dict | None = None): + """Handle re-auth if token invalid.""" + errors = {} + + if user_input is not None: + access_token = user_input[CONF_ACCESS_TOKEN] + _, error = await self._check_connection(access_token) + + if error is None: + entry = await self.async_set_unique_id(self.unique_id) + self.hass.config_entries.async_update_entry(entry, data=user_input) + return self.async_abort(reason="reauth_successful") + + if error != "invalid_access_token": + return self.async_abort(reason=error) + + errors = {CONF_ACCESS_TOKEN: error} + + return self.async_show_form( + step_id="reauth", + data_schema=vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}), + errors=errors, + ) + + async def _check_connection(self, access_token: str): + """Check the access token is valid.""" + session = async_get_clientsession(self.hass) + awair = Awair(access_token=access_token, session=session) + + try: + user = await awair.user() + devices = await user.devices() + if not devices: + return (None, "no_devices_found") + + return (user, None) + + except AuthError: + return (None, "invalid_access_token") + except AwairError as err: + LOGGER.error("Unexpected API error: %s", err) + return (None, "unknown") diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/const.py new file mode 100644 index 00000000000..2853ef9dd6c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/const.py @@ -0,0 +1,122 @@ +"""Constants for the Awair component.""" + +from dataclasses import dataclass +from datetime import timedelta +import logging + +from python_awair.devices import AwairDevice + +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_BILLION, + CONCENTRATION_PARTS_PER_MILLION, + DEVICE_CLASS_CO2, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_TEMPERATURE, + LIGHT_LUX, + PERCENTAGE, + TEMP_CELSIUS, +) + +API_CO2 = "carbon_dioxide" +API_DUST = "dust" +API_HUMID = "humidity" +API_LUX = "illuminance" +API_PM10 = "particulate_matter_10" +API_PM25 = "particulate_matter_2_5" +API_SCORE = "score" +API_SPL_A = "sound_pressure_level" +API_TEMP = "temperature" +API_TIMEOUT = 20 +API_VOC = "volatile_organic_compounds" + +ATTRIBUTION = "Awair air quality sensor" + +ATTR_LABEL = "label" +ATTR_UNIT = "unit" +ATTR_UNIQUE_ID = "unique_id" + +DOMAIN = "awair" + +DUST_ALIASES = [API_PM25, API_PM10] + +LOGGER = logging.getLogger(__package__) + +UPDATE_INTERVAL = timedelta(minutes=5) + +SENSOR_TYPES = { + API_SCORE: { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_UNIT: PERCENTAGE, + ATTR_LABEL: "Awair score", + ATTR_UNIQUE_ID: "score", # matches legacy format + }, + API_HUMID: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ICON: None, + ATTR_UNIT: PERCENTAGE, + ATTR_LABEL: "Humidity", + ATTR_UNIQUE_ID: "HUMID", # matches legacy format + }, + API_LUX: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE, + ATTR_ICON: None, + ATTR_UNIT: LIGHT_LUX, + ATTR_LABEL: "Illuminance", + ATTR_UNIQUE_ID: "illuminance", + }, + API_SPL_A: { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:ear-hearing", + ATTR_UNIT: "dBa", + ATTR_LABEL: "Sound level", + ATTR_UNIQUE_ID: "sound_level", + }, + API_VOC: { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:cloud", + ATTR_UNIT: CONCENTRATION_PARTS_PER_BILLION, + ATTR_LABEL: "Volatile organic compounds", + ATTR_UNIQUE_ID: "VOC", # matches legacy format + }, + API_TEMP: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_UNIT: TEMP_CELSIUS, + ATTR_LABEL: "Temperature", + ATTR_UNIQUE_ID: "TEMP", # matches legacy format + }, + API_PM25: { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_LABEL: "PM2.5", + ATTR_UNIQUE_ID: "PM25", # matches legacy format + }, + API_PM10: { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_LABEL: "PM10", + ATTR_UNIQUE_ID: "PM10", # matches legacy format + }, + API_CO2: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_CO2, + ATTR_ICON: "mdi:cloud", + ATTR_UNIT: CONCENTRATION_PARTS_PER_MILLION, + ATTR_LABEL: "Carbon dioxide", + ATTR_UNIQUE_ID: "CO2", # matches legacy format + }, +} + + +@dataclass +class AwairResult: + """Wrapper class to hold an awair device and set of air data.""" + + device: AwairDevice + air_data: dict diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/manifest.json new file mode 100644 index 00000000000..c1a3fbd59a7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "awair", + "name": "Awair", + "documentation": "https://www.home-assistant.io/integrations/awair", + "requirements": ["python_awair==0.2.1"], + "codeowners": ["@ahayworth", "@danielsjf"], + "config_flow": true, + "iot_class": "cloud_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/sensor.py new file mode 100644 index 00000000000..968587c3b10 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/sensor.py @@ -0,0 +1,238 @@ +"""Support for Awair sensors.""" +from __future__ import annotations + +from python_awair.devices import AwairDevice +import voluptuous as vol + +from homeassistant.components.awair import AwairDataUpdateCoordinator, AwairResult +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, CONF_ACCESS_TOKEN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ( + API_DUST, + API_PM25, + API_SCORE, + API_TEMP, + API_VOC, + ATTR_ICON, + ATTR_LABEL, + ATTR_UNIQUE_ID, + ATTR_UNIT, + ATTRIBUTION, + DOMAIN, + DUST_ALIASES, + LOGGER, + SENSOR_TYPES, +) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_ACCESS_TOKEN): cv.string}, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Import Awair configuration from YAML.""" + LOGGER.warning( + "Loading Awair via platform setup is deprecated; Please remove it from your configuration" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigType, + async_add_entities: AddEntitiesCallback, +): + """Set up Awair sensor entity based on a config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + sensors = [] + + data: list[AwairResult] = coordinator.data.values() + for result in data: + if result.air_data: + sensors.append(AwairSensor(API_SCORE, result.device, coordinator)) + device_sensors = result.air_data.sensors.keys() + for sensor in device_sensors: + if sensor in SENSOR_TYPES: + sensors.append(AwairSensor(sensor, result.device, coordinator)) + + # The "DUST" sensor for Awair is a combo pm2.5/pm10 sensor only + # present on first-gen devices in lieu of separate pm2.5/pm10 sensors. + # We handle that by creating fake pm2.5/pm10 sensors that will always + # report identical values, and we let users decide how they want to use + # that data - because we can't really tell what kind of particles the + # "DUST" sensor actually detected. However, it's still useful data. + if API_DUST in device_sensors: + for alias_kind in DUST_ALIASES: + sensors.append(AwairSensor(alias_kind, result.device, coordinator)) + + async_add_entities(sensors) + + +class AwairSensor(CoordinatorEntity, SensorEntity): + """Defines an Awair sensor entity.""" + + def __init__( + self, + kind: str, + device: AwairDevice, + coordinator: AwairDataUpdateCoordinator, + ) -> None: + """Set up an individual AwairSensor.""" + super().__init__(coordinator) + self._kind = kind + self._device = device + + @property + def name(self) -> str: + """Return the name of the sensor.""" + name = SENSOR_TYPES[self._kind][ATTR_LABEL] + if self._device.name: + name = f"{self._device.name} {name}" + + return name + + @property + def unique_id(self) -> str: + """Return the uuid as the unique_id.""" + unique_id_tag = SENSOR_TYPES[self._kind][ATTR_UNIQUE_ID] + + # This integration used to create a sensor that was labelled as a "PM2.5" + # sensor for first-gen Awair devices, but its unique_id reflected the truth: + # under the hood, it was a "DUST" sensor. So we preserve that specific unique_id + # for users with first-gen devices that are upgrading. + if self._kind == API_PM25 and API_DUST in self._air_data.sensors: + unique_id_tag = "DUST" + + return f"{self._device.uuid}_{unique_id_tag}" + + @property + def available(self) -> bool: + """Determine if the sensor is available based on API results.""" + # If the last update was successful... + if self.coordinator.last_update_success and self._air_data: + # and the results included our sensor type... + if self._kind in self._air_data.sensors: + # then we are available. + return True + + # or, we're a dust alias + if self._kind in DUST_ALIASES and API_DUST in self._air_data.sensors: + return True + + # or we are API_SCORE + if self._kind == API_SCORE: + # then we are available. + return True + + # Otherwise, we are not. + return False + + @property + def state(self) -> float: + """Return the state, rounding off to reasonable values.""" + state: float + + # Special-case for "SCORE", which we treat as the AQI + if self._kind == API_SCORE: + state = self._air_data.score + elif self._kind in DUST_ALIASES and API_DUST in self._air_data.sensors: + state = self._air_data.sensors.dust + else: + state = self._air_data.sensors[self._kind] + + if self._kind == API_VOC or self._kind == API_SCORE: + return round(state) + + if self._kind == API_TEMP: + return round(state, 1) + + return round(state, 2) + + @property + def icon(self) -> str: + """Return the icon.""" + return SENSOR_TYPES[self._kind][ATTR_ICON] + + @property + def device_class(self) -> str: + """Return the device_class.""" + return SENSOR_TYPES[self._kind][ATTR_DEVICE_CLASS] + + @property + def unit_of_measurement(self) -> str: + """Return the unit the value is expressed in.""" + return SENSOR_TYPES[self._kind][ATTR_UNIT] + + @property + def extra_state_attributes(self) -> dict: + """Return the Awair Index alongside state attributes. + + The Awair Index is a subjective score ranging from 0-4 (inclusive) that + is is used by the Awair app when displaying the relative "safety" of a + given measurement. Each value is mapped to a color indicating the safety: + + 0: green + 1: yellow + 2: light-orange + 3: orange + 4: red + + The API indicates that both positive and negative values may be returned, + but the negative values are mapped to identical colors as the positive values. + Knowing that, we just return the absolute value of a given index so that + users don't have to handle positive/negative values that ultimately "mean" + the same thing. + + https://docs.developer.getawair.com/?version=latest#awair-score-and-index + """ + attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} + if self._kind in self._air_data.indices: + attrs["awair_index"] = abs(self._air_data.indices[self._kind]) + elif self._kind in DUST_ALIASES and API_DUST in self._air_data.indices: + attrs["awair_index"] = abs(self._air_data.indices.dust) + + return attrs + + @property + def device_info(self) -> DeviceInfo: + """Device information.""" + info = { + "identifiers": {(DOMAIN, self._device.uuid)}, + "manufacturer": "Awair", + "model": self._device.model, + } + + if self._device.name: + info["name"] = self._device.name + + if self._device.mac_address: + info["connections"] = { + (dr.CONNECTION_NETWORK_MAC, self._device.mac_address) + } + + return info + + @property + def _air_data(self) -> AwairResult | None: + """Return the latest data for our device, or None.""" + result: AwairResult | None = self.coordinator.data.get(self._device.uuid) + if result: + return result.air_data + + return None diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/strings.json new file mode 100644 index 00000000000..f9b1f40e047 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/strings.json @@ -0,0 +1,29 @@ +{ + "config": { + "step": { + "user": { + "description": "You must register for an Awair developer access token at: https://developer.getawair.com/onboard/login", + "data": { + "access_token": "[%key:common::config_flow::data::access_token%]", + "email": "[%key:common::config_flow::data::email%]" + } + }, + "reauth": { + "description": "Please re-enter your Awair developer access token.", + "data": { + "access_token": "[%key:common::config_flow::data::access_token%]", + "email": "[%key:common::config_flow::data::email%]" + } + } + }, + "error": { + "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/ca.json new file mode 100644 index 00000000000..2e75af9e744 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/ca.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja ha estat configurat", + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "invalid_access_token": "Token d'acc\u00e9s no v\u00e0lid", + "unknown": "Error inesperat" + }, + "step": { + "reauth": { + "data": { + "access_token": "Token d'acc\u00e9s", + "email": "Correu electr\u00f2nic" + }, + "description": "Torna a introduir el token d'acc\u00e9s de desenvolupador d'Awair." + }, + "user": { + "data": { + "access_token": "Token d'acc\u00e9s", + "email": "Correu electr\u00f2nic" + }, + "description": "T'has de registrar a Awair per a obtenir un token d'acc\u00e9s de desenvolupador a trav\u00e9s de l'enlla\u00e7 seg\u00fcent: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/cs.json new file mode 100644 index 00000000000..dfc83778bf9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/cs.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "invalid_access_token": "Neplatn\u00fd p\u0159\u00edstupov\u00fd token", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "reauth": { + "data": { + "access_token": "P\u0159\u00edstupov\u00fd token", + "email": "E-mail" + } + }, + "user": { + "data": { + "access_token": "P\u0159\u00edstupov\u00fd token", + "email": "E-mail" + }, + "description": "Pro p\u0159\u00edstupov\u00fd token v\u00fdvoj\u00e1\u0159e Awair se mus\u00edte zaregistrovat na: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/de.json new file mode 100644 index 00000000000..1dacaf099dc --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/de.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "reauth": { + "data": { + "access_token": "Zugangstoken", + "email": "E-Mail" + }, + "description": "Bitte gib dein Awair-Entwicklerzugriffstoken erneut ein." + }, + "user": { + "data": { + "access_token": "Zugangstoken", + "email": "E-Mail" + }, + "description": "Du musst dich f\u00fcr ein Awair Entwickler-Zugangs-Token registrieren unter: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/en.json new file mode 100644 index 00000000000..0e5a1e62bb5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/en.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured", + "no_devices_found": "No devices found on the network", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "invalid_access_token": "Invalid access token", + "unknown": "Unexpected error" + }, + "step": { + "reauth": { + "data": { + "access_token": "Access Token", + "email": "Email" + }, + "description": "Please re-enter your Awair developer access token." + }, + "user": { + "data": { + "access_token": "Access Token", + "email": "Email" + }, + "description": "You must register for an Awair developer access token at: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/es.json new file mode 100644 index 00000000000..e87ce031b95 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/es.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya ha sido configurada", + "no_devices_found": "No se encontraron dispositivos en la red", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, + "error": { + "invalid_access_token": "Token de acceso no v\u00e1lido", + "unknown": "Error inesperado" + }, + "step": { + "reauth": { + "data": { + "access_token": "Token de acceso", + "email": "Correo electr\u00f3nico" + }, + "description": "Por favor, vuelve a introducir tu token de acceso de desarrollador Awair." + }, + "user": { + "data": { + "access_token": "Token de acceso", + "email": "Correo electr\u00f3nico" + }, + "description": "Debes registrarte para obtener un token de acceso de desarrollador Awair en: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/et.json new file mode 100644 index 00000000000..374db23e18e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/et.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba seadistatud", + "no_devices_found": "V\u00f5rgust ei leitud Awair seadmeid", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "invalid_access_token": "Vigane juurdep\u00e4\u00e4sut\u00f5end", + "unknown": "Tundmatu viga" + }, + "step": { + "reauth": { + "data": { + "access_token": "Juurdep\u00e4\u00e4sut\u00f5end", + "email": "E-post" + }, + "description": "Taassisesta oma Awairi arendaja juurdep\u00e4\u00e4suluba." + }, + "user": { + "data": { + "access_token": "Juurdep\u00e4\u00e4sut\u00f5end", + "email": "E-post" + }, + "description": "Pead registreerima Awair arendaja juurdep\u00e4\u00e4su loa aadressil: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/fr.json new file mode 100644 index 00000000000..dd90f940977 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/fr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "reauth_successful": "Jeton d'acc\u00e8s mis \u00e0 jour avec succ\u00e8s" + }, + "error": { + "invalid_access_token": "Jeton d'acc\u00e8s non valide", + "unknown": "Erreur d'API Awair inconnue." + }, + "step": { + "reauth": { + "data": { + "access_token": "Jeton d'acc\u00e8s", + "email": "Email" + }, + "description": "Veuillez ressaisir votre jeton d'acc\u00e8s d\u00e9veloppeur Awair." + }, + "user": { + "data": { + "access_token": "Jeton d'acc\u00e8s", + "email": "Email" + }, + "description": "Vous devez vous inscrire pour un jeton d'acc\u00e8s d\u00e9veloppeur Awair sur: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/hu.json new file mode 100644 index 00000000000..53827adf344 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/hu.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, + "error": { + "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "reauth": { + "data": { + "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", + "email": "E-mail" + }, + "description": "Add meg \u00fajra az Awair fejleszt\u0151i hozz\u00e1f\u00e9r\u00e9si tokent." + }, + "user": { + "data": { + "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", + "email": "E-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/id.json new file mode 100644 index 00000000000..2c6fab90909 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/id.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "invalid_access_token": "Token akses tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "reauth": { + "data": { + "access_token": "Token Akses", + "email": "Email" + }, + "description": "Masukkan kembali token akses pengembang Awair Anda." + }, + "user": { + "data": { + "access_token": "Token Akses", + "email": "Email" + }, + "description": "Anda harus mendaftar untuk mendapatkan token akses pengembang Awair di: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/it.json new file mode 100644 index 00000000000..cad2b8555a8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/it.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "invalid_access_token": "Token di accesso non valido", + "unknown": "Errore imprevisto" + }, + "step": { + "reauth": { + "data": { + "access_token": "Token di accesso", + "email": "E-mail" + }, + "description": "Inserisci nuovamente il tuo token di accesso per sviluppatori Awair." + }, + "user": { + "data": { + "access_token": "Token di accesso", + "email": "E-mail" + }, + "description": "\u00c8 necessario registrarsi per un token di accesso per sviluppatori Awair all'indirizzo: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/ko.json new file mode 100644 index 00000000000..22677f8ab45 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/ko.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "invalid_access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "reauth": { + "data": { + "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070", + "email": "\uc774\uba54\uc77c" + }, + "description": "Awair \uac1c\ubc1c\uc790 \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \ub2e4\uc2dc \uc785\ub825\ud574\uc8fc\uc138\uc694." + }, + "user": { + "data": { + "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070", + "email": "\uc774\uba54\uc77c" + }, + "description": "https://developer.getawair.com/onboard/login \uc5d0 Awair \uac1c\ubc1c\uc790 \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \ub4f1\ub85d\ud574\uc57c\ud569\ub2c8\ub2e4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/lb.json new file mode 100644 index 00000000000..cb2f758113a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/lb.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Kont ass", + "no_devices_found": "Keng Apparater am Netzwierk fonnt", + "reauth_successful": "Erfollegr\u00e4ich aktualis\u00e9iert" + }, + "error": { + "invalid_access_token": "Ong\u00ebltegen Acc\u00e8s jeton", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "reauth": { + "data": { + "access_token": "Acc\u00e8s Jeton", + "email": "E-Mail" + }, + "description": "G\u00ebff d\u00e4in Awair Developpeur Acc\u00e8s jeton nach emol un." + }, + "user": { + "data": { + "access_token": "Acc\u00e8s Jeton", + "email": "E-Mail" + }, + "description": "Du muss dech fir een Awair Developpeur Acc\u00e8s Jeton registr\u00e9ien op:\nhttps://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/nl.json new file mode 100644 index 00000000000..d41b85cc09b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/nl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "no_devices_found": "Geen apparaten op het netwerk gevonden", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "invalid_access_token": "Ongeldig toegangstoken", + "unknown": "Onverwachte fout" + }, + "step": { + "reauth": { + "data": { + "access_token": "Toegangstoken", + "email": "E-mail" + }, + "description": "Voer uw Awair-ontwikkelaarstoegangstoken opnieuw in." + }, + "user": { + "data": { + "access_token": "Toegangstoken", + "email": "E-mail" + }, + "description": "U moet zich registreren voor een Awair-toegangstoken voor ontwikkelaars op: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/no.json new file mode 100644 index 00000000000..98486a28b09 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/no.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "invalid_access_token": "Ugyldig tilgangstoken", + "unknown": "Uventet feil" + }, + "step": { + "reauth": { + "data": { + "access_token": "Tilgangstoken", + "email": "E-post" + }, + "description": "Skriv inn tilgangstokenet for Awair-utviklere p\u00e5 nytt." + }, + "user": { + "data": { + "access_token": "Tilgangstoken", + "email": "E-post" + }, + "description": "Du m\u00e5 registrere deg for et Awair-utviklertilgangstoken p\u00e5: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/pl.json new file mode 100644 index 00000000000..38bd24f714b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/pl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "invalid_access_token": "Niepoprawny token dost\u0119pu", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "reauth": { + "data": { + "access_token": "Token dost\u0119pu", + "email": "Adres e-mail" + }, + "description": "Wprowad\u017a ponownie token dost\u0119pu programisty Awair." + }, + "user": { + "data": { + "access_token": "Token dost\u0119pu", + "email": "Adres e-mail" + }, + "description": "Aby uzyska\u0107 token dost\u0119pu programisty Awair, nale\u017cy zarejestrowa\u0107 si\u0119 pod adresem: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/pt-BR.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/pt-BR.json new file mode 100644 index 00000000000..6cec4b5050d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/pt-BR.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_access_token": "token de acesso invalido" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/pt.json new file mode 100644 index 00000000000..ea99bbf0167 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/pt.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "reauth_successful": "Token de Acesso actualizado com sucesso" + }, + "error": { + "invalid_access_token": "Token de acesso inv\u00e1lido", + "unknown": "Erro inesperado" + }, + "step": { + "reauth": { + "data": { + "access_token": "Token de Acesso", + "email": "Email" + } + }, + "user": { + "data": { + "access_token": "Token de Acesso", + "email": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/ru.json new file mode 100644 index 00000000000..05a14ce7857 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/ru.json @@ -0,0 +1,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.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "reauth": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430." + }, + "user": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "description": "\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a Awair \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/tr.json new file mode 100644 index 00000000000..84da92b97d3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/tr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "invalid_access_token": "Ge\u00e7ersiz eri\u015fim belirteci", + "unknown": "Beklenmeyen hata" + }, + "step": { + "reauth": { + "data": { + "access_token": "Eri\u015fim Belirteci", + "email": "E-posta" + } + }, + "user": { + "data": { + "access_token": "Eri\u015fim Belirteci", + "email": "E-posta" + }, + "description": "Awair geli\u015ftirici eri\u015fim belirteci i\u00e7in \u015fu adresten kaydolmal\u0131s\u0131n\u0131z: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/uk.json new file mode 100644 index 00000000000..f8150ad7faf --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/uk.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "error": { + "invalid_access_token": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "reauth": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443", + "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + }, + "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443." + }, + "user": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443", + "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + }, + "description": "\u0414\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u0434\u043e Awair \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/zh-Hant.json new file mode 100644 index 00000000000..0bd7749c65f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/awair/translations/zh-Hant.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "invalid_access_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "reauth": { + "data": { + "access_token": "\u5b58\u53d6\u6b0a\u6756", + "email": "\u96fb\u5b50\u90f5\u4ef6" + }, + "description": "\u8acb\u91cd\u65b0\u8f38\u5165 Awair \u958b\u767c\u8005\u5b58\u53d6\u6b0a\u6756\u3002" + }, + "user": { + "data": { + "access_token": "\u5b58\u53d6\u6b0a\u6756", + "email": "\u96fb\u5b50\u90f5\u4ef6" + }, + "description": "\u5fc5\u9808\u5148\u8a3b\u518a Awair \u958b\u767c\u8005\u5b58\u53d6\u6b0a\u6756\uff1ahttps://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aws/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aws/__init__.py new file mode 100644 index 00000000000..da8c27d7445 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aws/__init__.py @@ -0,0 +1,178 @@ +"""Support for Amazon Web Services (AWS).""" +import asyncio +from collections import OrderedDict +import logging + +import aiobotocore +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + ATTR_CREDENTIALS, + CONF_NAME, + CONF_PROFILE_NAME, + CONF_SERVICE, +) +from homeassistant.helpers import config_validation as cv, discovery + +# Loading the config flow file will register the flow +from .const import ( + CONF_ACCESS_KEY_ID, + CONF_CONTEXT, + CONF_CREDENTIAL_NAME, + CONF_CREDENTIALS, + CONF_NOTIFY, + CONF_REGION, + CONF_SECRET_ACCESS_KEY, + CONF_VALIDATE, + DATA_CONFIG, + DATA_HASS_CONFIG, + DATA_SESSIONS, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +AWS_CREDENTIAL_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Inclusive(CONF_ACCESS_KEY_ID, ATTR_CREDENTIALS): cv.string, + vol.Inclusive(CONF_SECRET_ACCESS_KEY, ATTR_CREDENTIALS): cv.string, + vol.Exclusive(CONF_PROFILE_NAME, ATTR_CREDENTIALS): cv.string, + vol.Optional(CONF_VALIDATE, default=True): cv.boolean, + } +) + +DEFAULT_CREDENTIAL = [ + {CONF_NAME: "default", CONF_PROFILE_NAME: "default", CONF_VALIDATE: False} +] + +SUPPORTED_SERVICES = ["lambda", "sns", "sqs"] + +NOTIFY_PLATFORM_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_SERVICE): vol.All( + cv.string, vol.Lower, vol.In(SUPPORTED_SERVICES) + ), + vol.Required(CONF_REGION): vol.All(cv.string, vol.Lower), + vol.Inclusive(CONF_ACCESS_KEY_ID, ATTR_CREDENTIALS): cv.string, + vol.Inclusive(CONF_SECRET_ACCESS_KEY, ATTR_CREDENTIALS): cv.string, + vol.Exclusive(CONF_PROFILE_NAME, ATTR_CREDENTIALS): cv.string, + vol.Exclusive(CONF_CREDENTIAL_NAME, ATTR_CREDENTIALS): cv.string, + vol.Optional(CONF_CONTEXT): vol.Coerce(dict), + } +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_CREDENTIALS, default=DEFAULT_CREDENTIAL): vol.All( + cv.ensure_list, [AWS_CREDENTIAL_SCHEMA] + ), + vol.Optional(CONF_NOTIFY, default=[]): vol.All( + cv.ensure_list, [NOTIFY_PLATFORM_SCHEMA] + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up AWS component.""" + hass.data[DATA_HASS_CONFIG] = config + + conf = config.get(DOMAIN) + if conf is None: + # create a default conf using default profile + conf = CONFIG_SCHEMA({ATTR_CREDENTIALS: DEFAULT_CREDENTIAL}) + + hass.data[DATA_CONFIG] = conf + hass.data[DATA_SESSIONS] = OrderedDict() + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf + ) + ) + + return True + + +async def async_setup_entry(hass, entry): + """Load a config entry. + + Validate and save sessions per aws credential. + """ + config = hass.data.get(DATA_HASS_CONFIG) + conf = hass.data.get(DATA_CONFIG) + + if entry.source == config_entries.SOURCE_IMPORT: + if conf is None: + # user removed config from configuration.yaml, abort setup + hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) + return False + + if conf != entry.data: + # user changed config from configuration.yaml, use conf to setup + hass.config_entries.async_update_entry(entry, data=conf) + + if conf is None: + conf = CONFIG_SCHEMA({DOMAIN: entry.data})[DOMAIN] + + # validate credentials and create sessions + validation = True + tasks = [] + for cred in conf[ATTR_CREDENTIALS]: + tasks.append(_validate_aws_credentials(hass, cred)) + if tasks: + results = await asyncio.gather(*tasks, return_exceptions=True) + for index, result in enumerate(results): + name = conf[ATTR_CREDENTIALS][index][CONF_NAME] + if isinstance(result, Exception): + _LOGGER.error( + "Validating credential [%s] failed: %s", + name, + result, + exc_info=result, + ) + validation = False + else: + hass.data[DATA_SESSIONS][name] = result + + # set up notify platform, no entry support for notify component yet, + # have to use discovery to load platform. + for notify_config in conf[CONF_NOTIFY]: + hass.async_create_task( + discovery.async_load_platform(hass, "notify", DOMAIN, notify_config, config) + ) + + return validation + + +async def _validate_aws_credentials(hass, credential): + """Validate AWS credential config.""" + aws_config = credential.copy() + del aws_config[CONF_NAME] + del aws_config[CONF_VALIDATE] + + profile = aws_config.get(CONF_PROFILE_NAME) + + if profile is not None: + session = aiobotocore.AioSession(profile=profile) + del aws_config[CONF_PROFILE_NAME] + if CONF_ACCESS_KEY_ID in aws_config: + del aws_config[CONF_ACCESS_KEY_ID] + if CONF_SECRET_ACCESS_KEY in aws_config: + del aws_config[CONF_SECRET_ACCESS_KEY] + else: + session = aiobotocore.AioSession() + + if credential[CONF_VALIDATE]: + async with session.create_client("iam", **aws_config) as client: + await client.get_user() + + return session diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aws/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aws/config_flow.py new file mode 100644 index 00000000000..1854afc6231 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aws/config_flow.py @@ -0,0 +1,18 @@ +"""Config flow for AWS component.""" + +from homeassistant import config_entries + +from .const import DOMAIN + + +class AWSFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + + async def async_step_import(self, user_input): + """Import a config entry.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + return self.async_create_entry(title="configuration.yaml", data=user_input) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aws/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aws/const.py new file mode 100644 index 00000000000..8be6afec7ff --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aws/const.py @@ -0,0 +1,15 @@ +"""Constant for AWS component.""" +DOMAIN = "aws" + +DATA_CONFIG = "aws_config" +DATA_HASS_CONFIG = "aws_hass_config" +DATA_SESSIONS = "aws_sessions" + +CONF_ACCESS_KEY_ID = "aws_access_key_id" +CONF_CONTEXT = "context" +CONF_CREDENTIAL_NAME = "credential_name" +CONF_CREDENTIALS = "credentials" +CONF_NOTIFY = "notify" +CONF_REGION = "region_name" +CONF_SECRET_ACCESS_KEY = "aws_secret_access_key" +CONF_VALIDATE = "validate" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aws/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/aws/manifest.json new file mode 100644 index 00000000000..57f5558f0b1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aws/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "aws", + "name": "Amazon Web Services (AWS)", + "documentation": "https://www.home-assistant.io/integrations/aws", + "requirements": ["aiobotocore==1.2.2"], + "codeowners": [], + "iot_class": "cloud_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/aws/notify.py b/homeassistant-2021.6.0.dev0/homeassistant/components/aws/notify.py new file mode 100644 index 00000000000..c9d6ca2faa7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/aws/notify.py @@ -0,0 +1,231 @@ +"""AWS platform for notify component.""" +import asyncio +import base64 +import json +import logging + +import aiobotocore + +from homeassistant.components.notify import ( + ATTR_TARGET, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + BaseNotificationService, +) +from homeassistant.const import ( + CONF_NAME, + CONF_PLATFORM, + CONF_PROFILE_NAME, + CONF_SERVICE, +) +from homeassistant.helpers.json import JSONEncoder + +from .const import CONF_CONTEXT, CONF_CREDENTIAL_NAME, CONF_REGION, DATA_SESSIONS + +_LOGGER = logging.getLogger(__name__) + + +async def get_available_regions(hass, service): + """Get available regions for a service.""" + session = aiobotocore.get_session() + return await session.get_available_regions(service) + + +async def async_get_service(hass, config, discovery_info=None): + """Get the AWS notification service.""" + if discovery_info is None: + _LOGGER.error("Please config aws notify platform in aws component") + return None + + session = None + + conf = discovery_info + + service = conf[CONF_SERVICE] + region_name = conf[CONF_REGION] + + available_regions = await get_available_regions(hass, service) + if region_name not in available_regions: + _LOGGER.error( + "Region %s is not available for %s service, must in %s", + region_name, + service, + available_regions, + ) + return None + + aws_config = conf.copy() + + del aws_config[CONF_SERVICE] + del aws_config[CONF_REGION] + if CONF_PLATFORM in aws_config: + del aws_config[CONF_PLATFORM] + if CONF_NAME in aws_config: + del aws_config[CONF_NAME] + if CONF_CONTEXT in aws_config: + del aws_config[CONF_CONTEXT] + + if not aws_config: + # no platform config, use the first aws component credential instead + if hass.data[DATA_SESSIONS]: + session = next(iter(hass.data[DATA_SESSIONS].values())) + else: + _LOGGER.error("Missing aws credential for %s", config[CONF_NAME]) + return None + + if session is None: + credential_name = aws_config.get(CONF_CREDENTIAL_NAME) + if credential_name is not None: + session = hass.data[DATA_SESSIONS].get(credential_name) + if session is None: + _LOGGER.warning("No available aws session for %s", credential_name) + del aws_config[CONF_CREDENTIAL_NAME] + + if session is None: + profile = aws_config.get(CONF_PROFILE_NAME) + if profile is not None: + session = aiobotocore.AioSession(profile=profile) + del aws_config[CONF_PROFILE_NAME] + else: + session = aiobotocore.AioSession() + + aws_config[CONF_REGION] = region_name + + if service == "lambda": + context_str = json.dumps( + {"custom": conf.get(CONF_CONTEXT, {})}, cls=JSONEncoder + ) + context_b64 = base64.b64encode(context_str.encode("utf-8")) + context = context_b64.decode("utf-8") + return AWSLambda(session, aws_config, context) + + if service == "sns": + return AWSSNS(session, aws_config) + + if service == "sqs": + return AWSSQS(session, aws_config) + + # should not reach here since service was checked in schema + return None + + +class AWSNotify(BaseNotificationService): + """Implement the notification service for the AWS service.""" + + def __init__(self, session, aws_config): + """Initialize the service.""" + self.session = session + self.aws_config = aws_config + + +class AWSLambda(AWSNotify): + """Implement the notification service for the AWS Lambda service.""" + + service = "lambda" + + def __init__(self, session, aws_config, context): + """Initialize the service.""" + super().__init__(session, aws_config) + self.context = context + + async def async_send_message(self, message="", **kwargs): + """Send notification to specified LAMBDA ARN.""" + if not kwargs.get(ATTR_TARGET): + _LOGGER.error("At least one target is required") + return + + cleaned_kwargs = {k: v for k, v in kwargs.items() if v is not None} + payload = {"message": message} + payload.update(cleaned_kwargs) + json_payload = json.dumps(payload) + + async with self.session.create_client( + self.service, **self.aws_config + ) as client: + tasks = [] + for target in kwargs.get(ATTR_TARGET, []): + tasks.append( + client.invoke( + FunctionName=target, + Payload=json_payload, + ClientContext=self.context, + ) + ) + + if tasks: + await asyncio.gather(*tasks) + + +class AWSSNS(AWSNotify): + """Implement the notification service for the AWS SNS service.""" + + service = "sns" + + async def async_send_message(self, message="", **kwargs): + """Send notification to specified SNS ARN.""" + if not kwargs.get(ATTR_TARGET): + _LOGGER.error("At least one target is required") + return + + message_attributes = { + k: {"StringValue": json.dumps(v), "DataType": "String"} + for k, v in kwargs.items() + if v is not None + } + subject = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + + async with self.session.create_client( + self.service, **self.aws_config + ) as client: + tasks = [] + for target in kwargs.get(ATTR_TARGET, []): + tasks.append( + client.publish( + TargetArn=target, + Message=message, + Subject=subject, + MessageAttributes=message_attributes, + ) + ) + + if tasks: + await asyncio.gather(*tasks) + + +class AWSSQS(AWSNotify): + """Implement the notification service for the AWS SQS service.""" + + service = "sqs" + + async def async_send_message(self, message="", **kwargs): + """Send notification to specified SQS ARN.""" + if not kwargs.get(ATTR_TARGET): + _LOGGER.error("At least one target is required") + return + + cleaned_kwargs = {k: v for k, v in kwargs.items() if v is not None} + message_body = {"message": message} + message_body.update(cleaned_kwargs) + json_body = json.dumps(message_body) + message_attributes = {} + for key, val in cleaned_kwargs.items(): + message_attributes[key] = { + "StringValue": json.dumps(val), + "DataType": "String", + } + + async with self.session.create_client( + self.service, **self.aws_config + ) as client: + tasks = [] + for target in kwargs.get(ATTR_TARGET, []): + tasks.append( + client.send_message( + QueueUrl=target, + MessageBody=json_body, + MessageAttributes=message_attributes, + ) + ) + + if tasks: + await asyncio.gather(*tasks) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/__init__.py new file mode 100644 index 00000000000..e3c4d20fc04 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/__init__.py @@ -0,0 +1,78 @@ +"""Support for Axis devices.""" + +import logging + +from homeassistant.const import CONF_DEVICE, CONF_MAC, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import callback +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.entity_registry import async_migrate_entries + +from .const import DOMAIN as AXIS_DOMAIN +from .device import AxisNetworkDevice + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry): + """Set up the Axis component.""" + hass.data.setdefault(AXIS_DOMAIN, {}) + + device = AxisNetworkDevice(hass, config_entry) + + if not await device.async_setup(): + return False + + hass.data[AXIS_DOMAIN][config_entry.unique_id] = device + + await device.async_update_device_registry() + + config_entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.shutdown) + ) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload Axis device config entry.""" + device = hass.data[AXIS_DOMAIN].pop(config_entry.unique_id) + return await device.async_reset() + + +async def async_migrate_entry(hass, config_entry): + """Migrate old entry.""" + _LOGGER.debug("Migrating from version %s", config_entry.version) + + # Flatten configuration but keep old data if user rollbacks HASS prior to 0.106 + if config_entry.version == 1: + unique_id = config_entry.data[CONF_MAC] + data = {**config_entry.data, **config_entry.data[CONF_DEVICE]} + hass.config_entries.async_update_entry( + config_entry, unique_id=unique_id, data=data + ) + config_entry.version = 2 + + # Normalise MAC address of device which also affects entity unique IDs + if config_entry.version == 2: + old_unique_id = config_entry.unique_id + new_unique_id = format_mac(old_unique_id) + + @callback + def update_unique_id(entity_entry): + """Update unique ID of entity entry.""" + return { + "new_unique_id": entity_entry.unique_id.replace( + old_unique_id, new_unique_id + ) + } + + if old_unique_id != new_unique_id: + await async_migrate_entries(hass, config_entry.entry_id, update_unique_id) + + hass.config_entries.async_update_entry( + config_entry, unique_id=new_unique_id + ) + + _LOGGER.info("Migration to version %s successful", config_entry.version) + + return True diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/axis_base.py b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/axis_base.py new file mode 100644 index 00000000000..3e2b1a48eb7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/axis_base.py @@ -0,0 +1,76 @@ +"""Base classes for Axis entities.""" + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN as AXIS_DOMAIN + + +class AxisEntityBase(Entity): + """Base common to all Axis entities.""" + + def __init__(self, device): + """Initialize the Axis event.""" + self.device = device + + async def async_added_to_hass(self): + """Subscribe device events.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, self.device.signal_reachable, self.update_callback + ) + ) + + @property + def available(self): + """Return True if device is available.""" + return self.device.available + + @property + def device_info(self): + """Return a device description for device registry.""" + return {"identifiers": {(AXIS_DOMAIN, self.device.unique_id)}} + + @callback + def update_callback(self, no_delay=None): + """Update the entities state.""" + self.async_write_ha_state() + + +class AxisEventBase(AxisEntityBase): + """Base common to all Axis entities from event stream.""" + + def __init__(self, event, device): + """Initialize the Axis event.""" + super().__init__(device) + self.event = event + + async def async_added_to_hass(self) -> None: + """Subscribe sensors events.""" + self.event.register_callback(self.update_callback) + await super().async_added_to_hass() + + async def async_will_remove_from_hass(self) -> None: + """Disconnect device object when removed.""" + self.event.remove_callback(self.update_callback) + + @property + def device_class(self): + """Return the class of the event.""" + return self.event.CLASS + + @property + def name(self): + """Return the name of the event.""" + return f"{self.device.name} {self.event.TYPE} {self.event.id}" + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def unique_id(self): + """Return a unique identifier for this device.""" + return f"{self.device.unique_id}-{self.event.topic}-{self.event.id}" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/binary_sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/binary_sensor.py new file mode 100644 index 00000000000..222a356d4f9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/binary_sensor.py @@ -0,0 +1,134 @@ +"""Support for Axis binary sensors.""" + +from datetime import timedelta + +from axis.event_stream import ( + CLASS_INPUT, + CLASS_LIGHT, + CLASS_MOTION, + CLASS_OUTPUT, + CLASS_PTZ, + CLASS_SOUND, + FenceGuard, + LoiteringGuard, + MotionGuard, + ObjectAnalytics, + Vmd4, +) + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_SOUND, + BinarySensorEntity, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.util.dt import utcnow + +from .axis_base import AxisEventBase +from .const import DOMAIN as AXIS_DOMAIN + +DEVICE_CLASS = { + CLASS_INPUT: DEVICE_CLASS_CONNECTIVITY, + CLASS_LIGHT: DEVICE_CLASS_LIGHT, + CLASS_MOTION: DEVICE_CLASS_MOTION, + CLASS_SOUND: DEVICE_CLASS_SOUND, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a Axis binary sensor.""" + device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + + @callback + def async_add_sensor(event_id): + """Add binary sensor from Axis device.""" + event = device.api.event[event_id] + + if event.CLASS not in (CLASS_OUTPUT, CLASS_PTZ) and not ( + event.CLASS == CLASS_LIGHT and event.TYPE == "Light" + ): + async_add_entities([AxisBinarySensor(event, device)]) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, device.signal_new_event, async_add_sensor) + ) + + +class AxisBinarySensor(AxisEventBase, BinarySensorEntity): + """Representation of a binary Axis event.""" + + def __init__(self, event, device): + """Initialize the Axis binary sensor.""" + super().__init__(event, device) + self.cancel_scheduled_update = None + + @callback + def update_callback(self, no_delay=False): + """Update the sensor's state, if needed. + + Parameter no_delay is True when device_event_reachable is sent. + """ + + @callback + def scheduled_update(now): + """Timer callback for sensor update.""" + self.cancel_scheduled_update = None + self.async_write_ha_state() + + if self.cancel_scheduled_update is not None: + self.cancel_scheduled_update() + self.cancel_scheduled_update = None + + if self.is_on or self.device.option_trigger_time == 0 or no_delay: + self.async_write_ha_state() + return + + self.cancel_scheduled_update = async_track_point_in_utc_time( + self.hass, + scheduled_update, + utcnow() + timedelta(seconds=self.device.option_trigger_time), + ) + + @property + def is_on(self): + """Return true if event is active.""" + return self.event.is_tripped + + @property + def name(self): + """Return the name of the event.""" + if ( + self.event.CLASS == CLASS_INPUT + and self.event.id in self.device.api.vapix.ports + and self.device.api.vapix.ports[self.event.id].name + ): + return ( + f"{self.device.name} {self.device.api.vapix.ports[self.event.id].name}" + ) + + if self.event.CLASS == CLASS_MOTION: + + for event_class, event_data in ( + (FenceGuard, self.device.api.vapix.fence_guard), + (LoiteringGuard, self.device.api.vapix.loitering_guard), + (MotionGuard, self.device.api.vapix.motion_guard), + (ObjectAnalytics, self.device.api.vapix.object_analytics), + (Vmd4, self.device.api.vapix.vmd4), + ): + if ( + isinstance(self.event, event_class) + and event_data + and self.event.id in event_data + ): + return f"{self.device.name} {self.event.TYPE} {event_data[self.event.id].name}" + + return super().name + + @property + def device_class(self): + """Return the class of the sensor.""" + return DEVICE_CLASS.get(self.event.CLASS) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/camera.py b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/camera.py new file mode 100644 index 00000000000..cf2634b8f3a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/camera.py @@ -0,0 +1,116 @@ +"""Support for Axis camera streaming.""" + +from urllib.parse import urlencode + +from homeassistant.components.camera import SUPPORT_STREAM +from homeassistant.components.mjpeg.camera import ( + CONF_MJPEG_URL, + CONF_STILL_IMAGE_URL, + MjpegCamera, + filter_urllib3_logging, +) +from homeassistant.const import ( + CONF_AUTHENTICATION, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + HTTP_DIGEST_AUTHENTICATION, +) +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .axis_base import AxisEntityBase +from .const import DEFAULT_STREAM_PROFILE, DEFAULT_VIDEO_SOURCE, DOMAIN as AXIS_DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Axis camera video stream.""" + filter_urllib3_logging() + + device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + + if not device.api.vapix.params.image_format: + return + + async_add_entities([AxisCamera(device)]) + + +class AxisCamera(AxisEntityBase, MjpegCamera): + """Representation of a Axis camera.""" + + def __init__(self, device): + """Initialize Axis Communications camera component.""" + AxisEntityBase.__init__(self, device) + + config = { + CONF_NAME: device.name, + CONF_USERNAME: device.username, + CONF_PASSWORD: device.password, + CONF_MJPEG_URL: self.mjpeg_source, + CONF_STILL_IMAGE_URL: self.image_source, + CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION, + } + MjpegCamera.__init__(self, config) + + async def async_added_to_hass(self): + """Subscribe camera events.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, self.device.signal_new_address, self._new_address + ) + ) + + await super().async_added_to_hass() + + @property + def supported_features(self) -> int: + """Return supported features.""" + return SUPPORT_STREAM + + def _new_address(self) -> None: + """Set new device address for video stream.""" + self._mjpeg_url = self.mjpeg_source + self._still_image_url = self.image_source + + @property + def unique_id(self) -> str: + """Return a unique identifier for this device.""" + return f"{self.device.unique_id}-camera" + + @property + def image_source(self) -> str: + """Return still image URL for device.""" + options = self.generate_options(skip_stream_profile=True) + return f"http://{self.device.host}:{self.device.port}/axis-cgi/jpg/image.cgi{options}" + + @property + def mjpeg_source(self) -> str: + """Return mjpeg URL for device.""" + options = self.generate_options() + return f"http://{self.device.host}:{self.device.port}/axis-cgi/mjpg/video.cgi{options}" + + async def stream_source(self) -> str: + """Return the stream source.""" + options = self.generate_options(add_video_codec_h264=True) + return f"rtsp://{self.device.username}:{self.device.password}@{self.device.host}/axis-media/media.amp{options}" + + def generate_options( + self, skip_stream_profile: bool = False, add_video_codec_h264: bool = False + ) -> str: + """Generate options for video stream.""" + options_dict = {} + + if add_video_codec_h264: + options_dict["videocodec"] = "h264" + + if ( + not skip_stream_profile + and self.device.option_stream_profile != DEFAULT_STREAM_PROFILE + ): + options_dict["streamprofile"] = self.device.option_stream_profile + + if self.device.option_video_source != DEFAULT_VIDEO_SOURCE: + options_dict["camera"] = self.device.option_video_source + + if not options_dict: + return "" + return f"?{urlencode(options_dict)}" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/config_flow.py new file mode 100644 index 00000000000..8753114d86e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/config_flow.py @@ -0,0 +1,275 @@ +"""Config flow to configure Axis devices.""" + +from ipaddress import ip_address +from urllib.parse import urlsplit + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.config_entries import SOURCE_IGNORE +from homeassistant.const import ( + CONF_HOST, + CONF_MAC, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) +from homeassistant.core import callback +from homeassistant.helpers.device_registry import format_mac +from homeassistant.util.network import is_link_local + +from .const import ( + CONF_MODEL, + CONF_STREAM_PROFILE, + CONF_VIDEO_SOURCE, + DEFAULT_STREAM_PROFILE, + DEFAULT_VIDEO_SOURCE, + DOMAIN as AXIS_DOMAIN, +) +from .device import get_device +from .errors import AuthenticationRequired, CannotConnect + +AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f"} +DEFAULT_PORT = 80 + + +class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): + """Handle a Axis config flow.""" + + VERSION = 3 + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return AxisOptionsFlowHandler(config_entry) + + def __init__(self): + """Initialize the Axis config flow.""" + self.device_config = {} + self.discovery_schema = {} + self.import_schema = {} + self.serial = None + + async def async_step_user(self, user_input=None): + """Handle a Axis config flow start. + + Manage device specific parameters. + """ + errors = {} + + if user_input is not None: + try: + device = await get_device( + self.hass, + host=user_input[CONF_HOST], + port=user_input[CONF_PORT], + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], + ) + + self.serial = device.vapix.serial_number + await self.async_set_unique_id(format_mac(self.serial)) + + self._abort_if_unique_id_configured( + updates={ + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + } + ) + + self.device_config = { + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_MODEL: device.vapix.product_number, + } + + return await self._create_entry() + + except AuthenticationRequired: + errors["base"] = "invalid_auth" + + except CannotConnect: + errors["base"] = "cannot_connect" + + data = self.discovery_schema or { + vol.Required(CONF_HOST): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } + + return self.async_show_form( + step_id="user", + description_placeholders=self.device_config, + data_schema=vol.Schema(data), + errors=errors, + ) + + async def _create_entry(self): + """Create entry for device. + + Generate a name to be used as a prefix for device entities. + """ + model = self.device_config[CONF_MODEL] + same_model = [ + entry.data[CONF_NAME] + for entry in self.hass.config_entries.async_entries(AXIS_DOMAIN) + if entry.source != SOURCE_IGNORE and entry.data[CONF_MODEL] == model + ] + + name = model + for idx in range(len(same_model) + 1): + name = f"{model} {idx}" + if name not in same_model: + break + + self.device_config[CONF_NAME] = name + + title = f"{model} - {self.serial}" + return self.async_create_entry(title=title, data=self.device_config) + + async def async_step_reauth(self, device_config: dict): + """Trigger a reauthentication flow.""" + self.context["title_placeholders"] = { + CONF_NAME: device_config[CONF_NAME], + CONF_HOST: device_config[CONF_HOST], + } + + self.discovery_schema = { + vol.Required(CONF_HOST, default=device_config[CONF_HOST]): str, + vol.Required(CONF_USERNAME, default=device_config[CONF_USERNAME]): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_PORT, default=device_config[CONF_PORT]): int, + } + + return await self.async_step_user() + + async def async_step_dhcp(self, discovery_info: dict): + """Prepare configuration for a DHCP discovered Axis device.""" + return await self._process_discovered_device( + { + CONF_HOST: discovery_info[IP_ADDRESS], + CONF_MAC: format_mac(discovery_info.get(MAC_ADDRESS, "")), + CONF_NAME: discovery_info.get(HOSTNAME), + CONF_PORT: DEFAULT_PORT, + } + ) + + async def async_step_ssdp(self, discovery_info: dict): + """Prepare configuration for a SSDP discovered Axis device.""" + url = urlsplit(discovery_info["presentationURL"]) + return await self._process_discovered_device( + { + CONF_HOST: url.hostname, + CONF_MAC: format_mac(discovery_info["serialNumber"]), + CONF_NAME: f"{discovery_info['friendlyName']}", + CONF_PORT: url.port, + } + ) + + async def async_step_zeroconf(self, discovery_info: dict): + """Prepare configuration for a Zeroconf discovered Axis device.""" + return await self._process_discovered_device( + { + CONF_HOST: discovery_info[CONF_HOST], + CONF_MAC: format_mac(discovery_info["properties"]["macaddress"]), + CONF_NAME: discovery_info["name"].split(".", 1)[0], + CONF_PORT: discovery_info[CONF_PORT], + } + ) + + async def _process_discovered_device(self, device: dict): + """Prepare configuration for a discovered Axis device.""" + if device[CONF_MAC][:8] not in AXIS_OUI: + return self.async_abort(reason="not_axis_device") + + if is_link_local(ip_address(device[CONF_HOST])): + return self.async_abort(reason="link_local_address") + + await self.async_set_unique_id(device[CONF_MAC]) + + self._abort_if_unique_id_configured( + updates={ + CONF_HOST: device[CONF_HOST], + CONF_PORT: device[CONF_PORT], + } + ) + + self.context["title_placeholders"] = { + CONF_NAME: device[CONF_NAME], + CONF_HOST: device[CONF_HOST], + } + + self.discovery_schema = { + vol.Required(CONF_HOST, default=device[CONF_HOST]): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_PORT, default=device[CONF_PORT]): int, + } + + return await self.async_step_user() + + +class AxisOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Axis device options.""" + + def __init__(self, config_entry): + """Initialize Axis device options flow.""" + self.config_entry = config_entry + self.options = dict(config_entry.options) + self.device = None + + async def async_step_init(self, user_input=None): + """Manage the Axis device options.""" + self.device = self.hass.data[AXIS_DOMAIN][self.config_entry.unique_id] + return await self.async_step_configure_stream() + + async def async_step_configure_stream(self, user_input=None): + """Manage the Axis device stream options.""" + if user_input is not None: + self.options.update(user_input) + return self.async_create_entry(title="", data=self.options) + + schema = {} + + vapix = self.device.api.vapix + + # Stream profiles + + if vapix.params.stream_profiles_max_groups > 0: + + stream_profiles = [DEFAULT_STREAM_PROFILE] + for profile in vapix.streaming_profiles: + stream_profiles.append(profile.name) + + schema[ + vol.Optional( + CONF_STREAM_PROFILE, default=self.device.option_stream_profile + ) + ] = vol.In(stream_profiles) + + # Video sources + + if vapix.params.image_nbrofviews > 0: + await vapix.params.update_image() + + video_sources = {DEFAULT_VIDEO_SOURCE: DEFAULT_VIDEO_SOURCE} + for idx, video_source in vapix.params.image_sources.items(): + if not video_source["Enabled"]: + continue + video_sources[idx + 1] = video_source["Name"] + + schema[ + vol.Optional(CONF_VIDEO_SOURCE, default=self.device.option_video_source) + ] = vol.In(video_sources) + + return self.async_show_form( + step_id="configure_stream", data_schema=vol.Schema(schema) + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/const.py new file mode 100644 index 00000000000..a1ce77f099b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/const.py @@ -0,0 +1,25 @@ +"""Constants for the Axis component.""" +import logging + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN + +LOGGER = logging.getLogger(__package__) + +DOMAIN = "axis" + +ATTR_MANUFACTURER = "Axis Communications AB" + +CONF_EVENTS = "events" +CONF_MODEL = "model" +CONF_STREAM_PROFILE = "stream_profile" +CONF_VIDEO_SOURCE = "video_source" + +DEFAULT_EVENTS = True +DEFAULT_STREAM_PROFILE = "No stream profile" +DEFAULT_TRIGGER_TIME = 0 +DEFAULT_VIDEO_SOURCE = "No video source" + +PLATFORMS = [BINARY_SENSOR_DOMAIN, CAMERA_DOMAIN, LIGHT_DOMAIN, SWITCH_DOMAIN] diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/device.py b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/device.py new file mode 100644 index 00000000000..f1a57eec33c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/device.py @@ -0,0 +1,296 @@ +"""Axis network device abstraction.""" + +import asyncio + +import async_timeout +import axis +from axis.configuration import Configuration +from axis.errors import Unauthorized +from axis.event_stream import OPERATION_INITIALIZED +from axis.mqtt import mqtt_json_to_event +from axis.streammanager import SIGNAL_PLAYING, STATE_STOPPED + +from homeassistant.components import mqtt +from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN +from homeassistant.components.mqtt.models import Message +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_TRIGGER_TIME, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.httpx_client import get_async_client +from homeassistant.setup import async_when_setup + +from .const import ( + ATTR_MANUFACTURER, + CONF_EVENTS, + CONF_MODEL, + CONF_STREAM_PROFILE, + CONF_VIDEO_SOURCE, + DEFAULT_EVENTS, + DEFAULT_STREAM_PROFILE, + DEFAULT_TRIGGER_TIME, + DEFAULT_VIDEO_SOURCE, + DOMAIN as AXIS_DOMAIN, + LOGGER, + PLATFORMS, +) +from .errors import AuthenticationRequired, CannotConnect + + +class AxisNetworkDevice: + """Manages a Axis device.""" + + def __init__(self, hass, config_entry): + """Initialize the device.""" + self.hass = hass + self.config_entry = config_entry + self.available = True + + self.api = None + self.fw_version = None + self.product_type = None + + @property + def host(self): + """Return the host address of this device.""" + return self.config_entry.data[CONF_HOST] + + @property + def port(self): + """Return the HTTP port of this device.""" + return self.config_entry.data[CONF_PORT] + + @property + def username(self): + """Return the username of this device.""" + return self.config_entry.data[CONF_USERNAME] + + @property + def password(self): + """Return the password of this device.""" + return self.config_entry.data[CONF_PASSWORD] + + @property + def model(self): + """Return the model of this device.""" + return self.config_entry.data[CONF_MODEL] + + @property + def name(self): + """Return the name of this device.""" + return self.config_entry.data[CONF_NAME] + + @property + def unique_id(self): + """Return the unique ID (serial number) of this device.""" + return self.config_entry.unique_id + + # Options + + @property + def option_events(self): + """Config entry option defining if platforms based on events should be created.""" + return self.config_entry.options.get(CONF_EVENTS, DEFAULT_EVENTS) + + @property + def option_stream_profile(self): + """Config entry option defining what stream profile camera platform should use.""" + return self.config_entry.options.get( + CONF_STREAM_PROFILE, DEFAULT_STREAM_PROFILE + ) + + @property + def option_trigger_time(self): + """Config entry option defining minimum number of seconds to keep trigger high.""" + return self.config_entry.options.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME) + + @property + def option_video_source(self): + """Config entry option defining what video source camera platform should use.""" + return self.config_entry.options.get(CONF_VIDEO_SOURCE, DEFAULT_VIDEO_SOURCE) + + # Signals + + @property + def signal_reachable(self): + """Device specific event to signal a change in connection status.""" + return f"axis_reachable_{self.unique_id}" + + @property + def signal_new_event(self): + """Device specific event to signal new device event available.""" + return f"axis_new_event_{self.unique_id}" + + @property + def signal_new_address(self): + """Device specific event to signal a change in device address.""" + return f"axis_new_address_{self.unique_id}" + + # Callbacks + + @callback + def async_connection_status_callback(self, status): + """Handle signals of device connection status. + + This is called on every RTSP keep-alive message. + Only signal state change if state change is true. + """ + + if self.available != (status == SIGNAL_PLAYING): + self.available = not self.available + async_dispatcher_send(self.hass, self.signal_reachable, True) + + @callback + def async_event_callback(self, action, event_id): + """Call to configure events when initialized on event stream.""" + if action == OPERATION_INITIALIZED: + async_dispatcher_send(self.hass, self.signal_new_event, event_id) + + @staticmethod + async def async_new_address_callback(hass, entry): + """Handle signals of device getting new address. + + Called when config entry is updated. + This is a static method because a class method (bound method), + can not be used with weak references. + """ + device = hass.data[AXIS_DOMAIN][entry.unique_id] + device.api.config.host = device.host + async_dispatcher_send(hass, device.signal_new_address) + + async def async_update_device_registry(self): + """Update device registry.""" + device_registry = await self.hass.helpers.device_registry.async_get_registry() + device_registry.async_get_or_create( + config_entry_id=self.config_entry.entry_id, + connections={(CONNECTION_NETWORK_MAC, self.unique_id)}, + identifiers={(AXIS_DOMAIN, self.unique_id)}, + manufacturer=ATTR_MANUFACTURER, + model=f"{self.model} {self.product_type}", + name=self.name, + sw_version=self.fw_version, + ) + + async def use_mqtt(self, hass: HomeAssistant, component: str) -> None: + """Set up to use MQTT.""" + try: + status = await self.api.vapix.mqtt.get_client_status() + except Unauthorized: + # This means the user has too low privileges + status = {} + + if status.get("data", {}).get("status", {}).get("state") == "active": + self.config_entry.async_on_unload( + await mqtt.async_subscribe( + hass, f"{self.api.vapix.serial_number}/#", self.mqtt_message + ) + ) + + @callback + def mqtt_message(self, message: Message) -> None: + """Receive Axis MQTT message.""" + self.disconnect_from_stream() + + event = mqtt_json_to_event(message.payload) + self.api.event.update([event]) + + # Setup and teardown methods + + async def async_setup(self): + """Set up the device.""" + try: + self.api = await get_device( + self.hass, + host=self.host, + port=self.port, + username=self.username, + password=self.password, + ) + + except CannotConnect as err: + raise ConfigEntryNotReady from err + + except AuthenticationRequired as err: + raise ConfigEntryAuthFailed from err + + self.fw_version = self.api.vapix.firmware_version + self.product_type = self.api.vapix.product_type + + async def start_platforms(): + await asyncio.gather( + *[ + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, platform + ) + for platform in PLATFORMS + ] + ) + if self.option_events: + self.api.stream.connection_status_callback.append( + self.async_connection_status_callback + ) + self.api.enable_events(event_callback=self.async_event_callback) + self.api.stream.start() + + if self.api.vapix.mqtt: + async_when_setup(self.hass, MQTT_DOMAIN, self.use_mqtt) + + self.hass.async_create_task(start_platforms()) + + self.config_entry.add_update_listener(self.async_new_address_callback) + + return True + + @callback + def disconnect_from_stream(self): + """Stop stream.""" + if self.api.stream.state != STATE_STOPPED: + self.api.stream.connection_status_callback.clear() + self.api.stream.stop() + + async def shutdown(self, event): + """Stop the event stream.""" + self.disconnect_from_stream() + + async def async_reset(self): + """Reset this device to default state.""" + self.disconnect_from_stream() + + return await self.hass.config_entries.async_unload_platforms( + self.config_entry, PLATFORMS + ) + + +async def get_device(hass, host, port, username, password): + """Create a Axis device.""" + session = get_async_client(hass, verify_ssl=False) + + device = axis.AxisDevice( + Configuration(session, host, port=port, username=username, password=password) + ) + + try: + with async_timeout.timeout(30): + await device.vapix.initialize() + + return device + + except axis.Unauthorized as err: + LOGGER.warning("Connected to device at %s but not registered", host) + raise AuthenticationRequired from err + + except (asyncio.TimeoutError, axis.RequestError) as err: + LOGGER.error("Error connecting to the Axis device at %s", host) + raise CannotConnect from err + + except axis.AxisException as err: + LOGGER.exception("Unknown Axis communication error occurred") + raise AuthenticationRequired from err diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/errors.py b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/errors.py new file mode 100644 index 00000000000..56105b28b1b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/errors.py @@ -0,0 +1,22 @@ +"""Errors for the Axis component.""" +from homeassistant.exceptions import HomeAssistantError + + +class AxisException(HomeAssistantError): + """Base class for Axis exceptions.""" + + +class AlreadyConfigured(AxisException): + """Device is already configured.""" + + +class AuthenticationRequired(AxisException): + """Unknown error occurred.""" + + +class CannotConnect(AxisException): + """Unable to connect to the device.""" + + +class UserLevel(AxisException): + """User level too low.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/light.py b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/light.py new file mode 100644 index 00000000000..e627d6ccdbd --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/light.py @@ -0,0 +1,119 @@ +"""Support for Axis lights.""" + +from axis.event_stream import CLASS_LIGHT + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + SUPPORT_BRIGHTNESS, + LightEntity, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .axis_base import AxisEventBase +from .const import DOMAIN as AXIS_DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a Axis light.""" + device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + + if ( + device.api.vapix.light_control is None + or len(device.api.vapix.light_control) == 0 + ): + return + + @callback + def async_add_sensor(event_id): + """Add light from Axis device.""" + event = device.api.event[event_id] + + if event.CLASS == CLASS_LIGHT and event.TYPE == "Light": + async_add_entities([AxisLight(event, device)]) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, device.signal_new_event, async_add_sensor) + ) + + +class AxisLight(AxisEventBase, LightEntity): + """Representation of a light Axis event.""" + + def __init__(self, event, device): + """Initialize the Axis light.""" + super().__init__(event, device) + + self.light_id = f"led{self.event.id}" + + self.current_intensity = 0 + self.max_intensity = 0 + + self._features = SUPPORT_BRIGHTNESS + + async def async_added_to_hass(self) -> None: + """Subscribe lights events.""" + await super().async_added_to_hass() + + current_intensity = ( + await self.device.api.vapix.light_control.get_current_intensity( + self.light_id + ) + ) + self.current_intensity = current_intensity["data"]["intensity"] + + max_intensity = await self.device.api.vapix.light_control.get_valid_intensity( + self.light_id + ) + self.max_intensity = max_intensity["data"]["ranges"][0]["high"] + + @property + def supported_features(self): + """Flag supported features.""" + return self._features + + @property + def name(self): + """Return the name of the light.""" + light_type = self.device.api.vapix.light_control[self.light_id].light_type + return f"{self.device.name} {light_type} {self.event.TYPE} {self.event.id}" + + @property + def is_on(self): + """Return true if light is on.""" + return self.event.is_tripped + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return int((self.current_intensity / self.max_intensity) * 255) + + async def async_turn_on(self, **kwargs): + """Turn on light.""" + if not self.is_on: + await self.device.api.vapix.light_control.activate_light(self.light_id) + + if ATTR_BRIGHTNESS in kwargs: + intensity = int((kwargs[ATTR_BRIGHTNESS] / 255) * self.max_intensity) + await self.device.api.vapix.light_control.set_manual_intensity( + self.light_id, intensity + ) + + async def async_turn_off(self, **kwargs): + """Turn off light.""" + if self.is_on: + await self.device.api.vapix.light_control.deactivate_light(self.light_id) + + async def async_update(self): + """Update brightness.""" + current_intensity = ( + await self.device.api.vapix.light_control.get_current_intensity( + self.light_id + ) + ) + self.current_intensity = current_intensity["data"]["intensity"] + + @property + def should_poll(self): + """Brightness needs polling.""" + return True diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/manifest.json new file mode 100644 index 00000000000..52e0c99044b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/manifest.json @@ -0,0 +1,44 @@ +{ + "domain": "axis", + "name": "Axis", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/axis", + "requirements": ["axis==44"], + "dhcp": [ + { + "hostname": "axis-00408c*", + "macaddress": "00408C*" + }, + { + "hostname": "axis-accc8e*", + "macaddress": "ACCC8E*" + }, + { + "hostname": "axis-b8a44f*", + "macaddress": "B8A44F*" + } + ], + "ssdp": [ + { + "manufacturer": "AXIS" + } + ], + "zeroconf": [ + { + "type": "_axis-video._tcp.local.", + "macaddress": "00408C*" + }, + { + "type": "_axis-video._tcp.local.", + "macaddress": "ACCC8E*" + }, + { + "type": "_axis-video._tcp.local.", + "macaddress": "B8A44F*" + } + ], + "after_dependencies": ["mqtt"], + "codeowners": ["@Kane610"], + "quality_scale": "platinum", + "iot_class": "local_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/strings.json new file mode 100644 index 00000000000..47a25b542a7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/strings.json @@ -0,0 +1,37 @@ +{ + "config": { + "flow_title": "{name} ({host})", + "step": { + "user": { + "title": "Set up Axis device", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "port": "[%key:common::config_flow::data::port%]" + } + } + }, + "error": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "link_local_address": "Link local addresses are not supported", + "not_axis_device": "Discovered device not an Axis device" + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Select stream profile to use" + }, + "title": "Axis device video stream options" + } + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/switch.py b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/switch.py new file mode 100644 index 00000000000..e509716fc1f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/switch.py @@ -0,0 +1,54 @@ +"""Support for Axis switches.""" + +from axis.event_stream import CLASS_OUTPUT + +from homeassistant.components.switch import SwitchEntity +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .axis_base import AxisEventBase +from .const import DOMAIN as AXIS_DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a Axis switch.""" + device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + + @callback + def async_add_switch(event_id): + """Add switch from Axis device.""" + event = device.api.event[event_id] + + if event.CLASS == CLASS_OUTPUT: + async_add_entities([AxisSwitch(event, device)]) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, device.signal_new_event, async_add_switch) + ) + + +class AxisSwitch(AxisEventBase, SwitchEntity): + """Representation of a Axis switch.""" + + @property + def is_on(self): + """Return true if event is active.""" + return self.event.is_tripped + + async def async_turn_on(self, **kwargs): + """Turn on switch.""" + await self.device.api.vapix.ports[self.event.id].close() + + async def async_turn_off(self, **kwargs): + """Turn off switch.""" + await self.device.api.vapix.ports[self.event.id].open() + + @property + def name(self): + """Return the name of the event.""" + if self.event.id and self.device.api.vapix.ports[self.event.id].name: + return ( + f"{self.device.name} {self.device.api.vapix.ports[self.event.id].name}" + ) + + return super().name diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/bg.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/bg.json new file mode 100644 index 00000000000..2cbf383cea8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/bg.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "link_local_address": "\u041b\u043e\u043a\u0430\u043b\u043d\u0438 \u0430\u0434\u0440\u0435\u0441\u0438 \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u0442", + "not_axis_device": "\u041e\u0442\u043a\u0440\u0438\u0442\u043e\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0435 Axis" + }, + "error": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "already_in_progress": "\u0412 \u043c\u043e\u043c\u0435\u043d\u0442\u0430 \u0442\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e." + }, + "flow_title": "Axis \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e: {name} ({host})", + "step": { + "user": { + "data": { + "host": "\u0410\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043e\u0442 Axis" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/ca.json new file mode 100644 index 00000000000..3e104c1005e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/ca.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "link_local_address": "L'enlla\u00e7 d'adreces locals no est\u00e0 disponible", + "not_axis_device": "El dispositiu descobert no \u00e9s un dispositiu Axis" + }, + "error": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya", + "port": "Port", + "username": "Nom d'usuari" + }, + "title": "Configuraci\u00f3 de dispositiu Axis" + } + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Selecciona el perfil de transmissi\u00f3 de v\u00eddeo a utilitzar" + }, + "title": "Opcions de transmissi\u00f3 de v\u00eddeo del dispositiu Axis" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/cs.json new file mode 100644 index 00000000000..4f7c3016235 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/cs.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "link_local_address": "Propojen\u00ed m\u00edstn\u00edch adres nen\u00ed podporov\u00e1no", + "not_axis_device": "Objeven\u00e9 za\u0159\u00edzen\u00ed nen\u00ed za\u0159\u00edzen\u00ed Axis" + }, + "error": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + }, + "title": "Nastaven\u00ed za\u0159\u00edzen\u00ed Axis" + } + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Vyberte profil streamu, kter\u00fd chcete pou\u017e\u00edt" + }, + "title": "Mo\u017enosti video streamu za\u0159\u00edzen\u00ed Axis" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/da.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/da.json new file mode 100644 index 00000000000..e449ac98053 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/da.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Enheden er allerede konfigureret", + "link_local_address": "Link lokale adresser underst\u00f8ttes ikke", + "not_axis_device": "Fundet enhed ikke en Axis enhed" + }, + "error": { + "already_configured": "Enheden er allerede konfigureret", + "already_in_progress": "Enhedskonfiguration er allerede i gang." + }, + "flow_title": "Axis-enhed: {name} ({host})", + "step": { + "user": { + "data": { + "host": "V\u00e6rt", + "password": "Adgangskode", + "port": "Port", + "username": "Brugernavn" + }, + "title": "Indstil Axis-enhed" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/de.json new file mode 100644 index 00000000000..ed95dea6fc1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/de.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "link_local_address": "Link-local Adressen werden nicht unterst\u00fctzt", + "not_axis_device": "Erkanntes Ger\u00e4t ist kein Axis-Ger\u00e4t" + }, + "error": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "flow_title": "Achsenger\u00e4t: {name} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" + }, + "title": "Axis Ger\u00e4t einrichten" + } + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Zu verwendendes Stream-Profil ausw\u00e4hlen" + }, + "title": "Optionen des Axis Videostream-Ger\u00e4ts" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/en.json new file mode 100644 index 00000000000..f71e91f6280 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/en.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "link_local_address": "Link local addresses are not supported", + "not_axis_device": "Discovered device not an Axis device" + }, + "error": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Port", + "username": "Username" + }, + "title": "Set up Axis device" + } + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Select stream profile to use" + }, + "title": "Axis device video stream options" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/es-419.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/es-419.json new file mode 100644 index 00000000000..0e1c1e99b36 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/es-419.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "link_local_address": "Las direcciones locales de enlace no son compatibles", + "not_axis_device": "El dispositivo descubierto no es un dispositivo de Axis" + }, + "error": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ya est\u00e1 en progreso." + }, + "flow_title": "Dispositivo Axis: {name} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Nombre de usuario" + }, + "title": "Configurar dispositivo Axis" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/es.json new file mode 100644 index 00000000000..4a47b17c528 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/es.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "link_local_address": "Las direcciones de enlace locales no son compatibles", + "not_axis_device": "El dispositivo descubierto no es un dispositivo de Axis" + }, + "error": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n del dispositivo ya est\u00e1 en marcha.", + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, + "flow_title": "Dispositivo Axis: {name} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Usuario" + }, + "title": "Configurar dispositivo Axis" + } + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Selecciona el perfil de transmisi\u00f3n a usar" + }, + "title": "Opciones de transmisi\u00f3n de v\u00eddeo del dispositivo Axis" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/et.json new file mode 100644 index 00000000000..f6f9a523cb6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/et.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "link_local_address": "Kohtv\u00f5rgu linke ei toetata", + "not_axis_device": "Avastatud seade pole Axise seade" + }, + "error": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamise viga" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "", + "password": "Salas\u00f5na", + "port": "", + "username": "Kasutajanimi" + }, + "title": "Seadista Axis'e seade" + } + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Vali kasutatav vooprofiil" + }, + "title": "Axis'e seadme videovoo valikud" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/fi.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/fi.json new file mode 100644 index 00000000000..a3740dd8bcf --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/fi.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Asenna Axis-laite" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/fr.json new file mode 100644 index 00000000000..ed4113d02e2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/fr.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "link_local_address": "Les adresses locales ne sont pas prises en charge", + "not_axis_device": "L'appareil d\u00e9couvert n'est pas un appareil Axis" + }, + "error": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "Le flux de configuration de l'appareil est d\u00e9j\u00e0 en cours.", + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide" + }, + "flow_title": "Appareil Axis: {name} ( {host} )", + "step": { + "user": { + "data": { + "host": "Nom d'h\u00f4te ou adresse IP", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur" + }, + "title": "Configurer l'appareil Axis" + } + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "S\u00e9lectionnez le profil de flux \u00e0 utiliser" + }, + "title": "Options de flux vid\u00e9o du p\u00e9riph\u00e9rique Axis" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/he.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/he.json new file mode 100644 index 00000000000..3007c0e968c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/hu.json new file mode 100644 index 00000000000..972690ede97 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/hu.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "flow_title": "Axis eszk\u00f6z: {name} ({host})", + "step": { + "user": { + "data": { + "host": "Hoszt", + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "V\u00e1lassza ki a haszn\u00e1lni k\u00edv\u00e1nt adatfolyam-profilt" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/id.json new file mode 100644 index 00000000000..cdd498a8e6c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/id.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "link_local_address": "Tautan alamat lokal tidak didukung", + "not_axis_device": "Perangkat yang ditemukan bukan perangkat Axis" + }, + "error": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "port": "Port", + "username": "Nama Pengguna" + }, + "title": "Siapkan perangkat Axis" + } + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Pilih profil streaming yang akan digunakan" + }, + "title": "Opsi streaming video perangkat Axis" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/it.json new file mode 100644 index 00000000000..7e7aeb1d1b2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/it.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "link_local_address": "Gli indirizzi locali di collegamento non sono supportati", + "not_axis_device": "Il dispositivo rilevato non \u00e8 un dispositivo Axis" + }, + "error": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Porta", + "username": "Nome utente" + }, + "title": "Impostazione del dispositivo Axis" + } + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Selezionare il profilo di flusso da utilizzare" + }, + "title": "Opzioni del flusso video del dispositivo Axis" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/ko.json new file mode 100644 index 00000000000..d9e0114a97a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/ko.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "link_local_address": "\ub85c\uceec \uc8fc\uc18c \uc5f0\uacb0\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "not_axis_device": "\ubc1c\uacac\ub41c \uae30\uae30\ub294 Axis \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4" + }, + "error": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "title": "Axis \uae30\uae30 \uc124\uc815\ud558\uae30" + } + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "\uc0ac\uc6a9\ud560 \uc2a4\ud2b8\ub9bc \ud504\ub85c\ud544\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694" + }, + "title": "Axis \uae30\uae30 \ube44\ub514\uc624 \uc2a4\ud2b8\ub9bc \uc635\uc158" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/lb.json new file mode 100644 index 00000000000..ae5bd8dc7a0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/lb.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "link_local_address": "Lokal Link Adressen ginn net \u00ebnnerst\u00ebtzt", + "not_axis_device": "Entdeckten Apparat ass keen Axis Apparat" + }, + "error": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "already_in_progress": "Konfiguratioun's Oflas ass schon am gaang", + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun" + }, + "flow_title": "Axis Apparat: {name} ({host})", + "step": { + "user": { + "data": { + "host": "Apparat", + "password": "Passwuert", + "port": "Port", + "username": "Benotzernumm" + }, + "title": "Axis Apparat ariichten" + } + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Stream Profile auswielen" + }, + "title": "Axis Apparat Video Stream Optiounen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/nl.json new file mode 100644 index 00000000000..3b41c1184ba --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/nl.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "link_local_address": "Link-lokale adressen worden niet ondersteund", + "not_axis_device": "Ontdekte apparaat, is geen Axis-apparaat" + }, + "error": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam" + }, + "title": "Stel het Axis-apparaat in" + } + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Selecteer stream profiel om te gebruiken" + }, + "title": "Opties voor videostreams van Axis-apparaten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/nn.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/nn.json new file mode 100644 index 00000000000..b6296d1acab --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/nn.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord", + "port": "Port", + "username": "Brukarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/no.json new file mode 100644 index 00000000000..1fc0640eb9b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/no.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "link_local_address": "Linking av lokale adresser st\u00f8ttes ikke", + "not_axis_device": "Oppdaget enhet ikke en Axis enhet" + }, + "error": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" + }, + "flow_title": "{name} ( {host} )", + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord", + "port": "Port", + "username": "Brukernavn" + }, + "title": "Sett opp Axis enhet" + } + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Velg str\u00f8mprofil som skal brukes" + }, + "title": "Axis videostr\u00f8m alternativer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/pl.json new file mode 100644 index 00000000000..e44816bc2ea --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/pl.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "link_local_address": "Po\u0142\u0105czenie lokalnego adresu nie jest obs\u0142ugiwane", + "not_axis_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem Axis" + }, + "error": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika" + }, + "title": "Konfiguracja urz\u0105dzenia Axis" + } + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Wybierz profil strumienia do u\u017cycia" + }, + "title": "Opcje strumienia wideo urz\u0105dzenia Axis" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/pt-BR.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/pt-BR.json new file mode 100644 index 00000000000..86b6d408baa --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/pt-BR.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "link_local_address": "Link de endere\u00e7os locais n\u00e3o s\u00e3o suportados", + "not_axis_device": "Dispositivo descoberto n\u00e3o \u00e9 um dispositivo Axis" + }, + "error": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o para o dispositivo j\u00e1 est\u00e1 em andamento." + }, + "flow_title": "Eixos do dispositivo: {name} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Senha", + "port": "Porta", + "username": "Usu\u00e1rio" + }, + "title": "Configurar o dispositivo Axis" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/pt.json new file mode 100644 index 00000000000..8ba642263a4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/pt.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "link_local_address": "Eendere\u00e7os de liga\u00e7\u00e3o local n\u00e3o s\u00e3o suportados" + }, + "error": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "password": "Palavra-passe", + "port": "Porta", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/ru.json new file mode 100644 index 00000000000..f5c5e79a32f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/ru.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", + "not_axis_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Axis." + }, + "error": { + "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_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "title": "Axis" + } + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u044c \u043f\u043e\u0442\u043e\u043a\u0430 \u0434\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0432\u0438\u0434\u0435\u043e \u043f\u043e\u0442\u043e\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Axis" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/sl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/sl.json new file mode 100644 index 00000000000..fb7198b2f32 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/sl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana", + "link_local_address": "Lokalni naslovi povezave niso podprti", + "not_axis_device": "Odkrita naprava ni naprava Axis" + }, + "error": { + "already_configured": "Naprava je \u017ee konfigurirana", + "already_in_progress": "Konfiguracijski tok za to napravo je \u017ee v teku." + }, + "flow_title": "OS naprava: {Name} ({Host})", + "step": { + "user": { + "data": { + "host": "Gostitelj", + "password": "Geslo", + "port": "Vrata", + "username": "Uporabni\u0161ko ime" + }, + "title": "Nastavite plo\u0161\u010dek" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/sv.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/sv.json new file mode 100644 index 00000000000..22e9344b64d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/sv.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "link_local_address": "Link local addresses are not supported", + "not_axis_device": "Uppt\u00e4ckte enhet som inte \u00e4r en Axis enhet" + }, + "error": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurations fl\u00f6det f\u00f6r enheten p\u00e5g\u00e5r redan." + }, + "flow_title": "Axisenhet: {name} ({host})", + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "password": "L\u00f6senord", + "port": "Port", + "username": "Anv\u00e4ndarnamn" + }, + "title": "Konfigurera Axis-enhet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/th.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/th.json new file mode 100644 index 00000000000..4226d4ddb3e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/th.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u0e23\u0e2b\u0e31\u0e2a\u0e1c\u0e48\u0e32\u0e19", + "port": "Port", + "username": "\u0e0a\u0e37\u0e48\u0e2d\u0e1c\u0e39\u0e49\u0e43\u0e0a\u0e49" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/tr.json new file mode 100644 index 00000000000..b2d609747d1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/tr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/uk.json new file mode 100644 index 00000000000..35b849ce968 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/uk.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "link_local_address": "\u041f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0456 \u0430\u0434\u0440\u0435\u0441\u0438 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f.", + "not_axis_device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0454 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c Axis." + }, + "error": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "flow_title": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Axis {name} ({host})", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "Axis" + } + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u043e\u0444\u0456\u043b\u044c \u043f\u043e\u0442\u043e\u043a\u0443 \u0434\u043b\u044f \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0432\u0456\u0434\u0435\u043e\u043f\u043e\u0442\u043e\u043a\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Axis" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/zh-Hans.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/zh-Hans.json new file mode 100644 index 00000000000..0ed34907b1a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/zh-Hans.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u9a8c\u8bc1\u7801\u65e0\u6548" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u673a\u7aef", + "password": "\u5bc6\u7801", + "port": "\u7aef\u53e3", + "username": "\u7528\u6237\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/zh-Hant.json new file mode 100644 index 00000000000..892cb8fb6df --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/axis/translations/zh-Hant.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740", + "not_axis_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Axis \u88dd\u7f6e" + }, + "error": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "title": "\u8a2d\u5b9a Axis \u88dd\u7f6e" + } + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "\u9078\u64c7\u6240\u8981\u4f7f\u7528\u7684\u4e32\u6d41\u8a2d\u5b9a" + }, + "title": "Axis \u88dd\u7f6e\u5f71\u50cf\u4e32\u6d41\u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/__init__.py new file mode 100644 index 00000000000..ba9020e3e88 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/__init__.py @@ -0,0 +1,118 @@ +"""Support for Azure DevOps.""" +from __future__ import annotations + +import logging + +from aioazuredevops.client import DevOpsClient +import aiohttp + +from homeassistant.components.azure_devops.const import ( + CONF_ORG, + CONF_PAT, + CONF_PROJECT, + DATA_AZURE_DEVOPS_CLIENT, + DOMAIN, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.entity import DeviceInfo, Entity + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["sensor"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Azure DevOps from a config entry.""" + client = DevOpsClient() + + try: + if entry.data[CONF_PAT] is not None: + await client.authorize(entry.data[CONF_PAT], entry.data[CONF_ORG]) + if not client.authorized: + raise ConfigEntryAuthFailed( + "Could not authorize with Azure DevOps. You may need to update your token" + ) + await client.get_project(entry.data[CONF_ORG], entry.data[CONF_PROJECT]) + except aiohttp.ClientError as exception: + _LOGGER.warning(exception) + raise ConfigEntryNotReady from exception + + instance_key = f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}" + hass.data.setdefault(instance_key, {})[DATA_AZURE_DEVOPS_CLIENT] = client + + # Setup components + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Azure DevOps config entry.""" + del hass.data[f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}"] + + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +class AzureDevOpsEntity(Entity): + """Defines a base Azure DevOps entity.""" + + def __init__(self, organization: str, project: str, name: str, icon: str) -> None: + """Initialize the Azure DevOps entity.""" + self._name = name + self._icon = icon + self._available = True + self.organization = organization + self.project = project + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def icon(self) -> str: + """Return the mdi icon of the entity.""" + return self._icon + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + async def async_update(self) -> None: + """Update Azure DevOps entity.""" + if await self._azure_devops_update(): + self._available = True + else: + if self._available: + _LOGGER.debug( + "An error occurred while updating Azure DevOps sensor", + exc_info=True, + ) + self._available = False + + async def _azure_devops_update(self) -> None: + """Update Azure DevOps entity.""" + raise NotImplementedError() + + +class AzureDevOpsDeviceEntity(AzureDevOpsEntity): + """Defines a Azure DevOps device entity.""" + + @property + def device_info(self) -> DeviceInfo: + """Return device information about this Azure DevOps instance.""" + return { + "identifiers": { + ( + DOMAIN, + self.organization, + self.project, + ) + }, + "manufacturer": self.organization, + "name": self.project, + "entry_type": "service", + } diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/config_flow.py new file mode 100644 index 00000000000..30073031195 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/config_flow.py @@ -0,0 +1,126 @@ +"""Config flow to configure the Azure DevOps integration.""" +from aioazuredevops.client import DevOpsClient +import aiohttp +import voluptuous as vol + +from homeassistant.components.azure_devops.const import ( + CONF_ORG, + CONF_PAT, + CONF_PROJECT, + DOMAIN, +) +from homeassistant.config_entries import ConfigFlow + + +class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): + """Handle a Azure DevOps config flow.""" + + VERSION = 1 + + def __init__(self): + """Initialize config flow.""" + self._organization = None + self._project = None + self._pat = None + + async def _show_setup_form(self, errors=None): + """Show the setup form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_ORG, default=self._organization): str, + vol.Required(CONF_PROJECT, default=self._project): str, + vol.Optional(CONF_PAT): str, + } + ), + errors=errors or {}, + ) + + async def _show_reauth_form(self, errors=None): + """Show the reauth form to the user.""" + return self.async_show_form( + step_id="reauth", + description_placeholders={ + "project_url": f"{self._organization}/{self._project}" + }, + data_schema=vol.Schema({vol.Required(CONF_PAT): str}), + errors=errors or {}, + ) + + async def _check_setup(self): + """Check the setup of the flow.""" + errors = {} + + client = DevOpsClient() + + try: + if self._pat is not None: + await client.authorize(self._pat, self._organization) + if not client.authorized: + errors["base"] = "invalid_auth" + return errors + project_info = await client.get_project(self._organization, self._project) + if project_info is None: + errors["base"] = "project_error" + return errors + except aiohttp.ClientError: + errors["base"] = "cannot_connect" + return errors + return None + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + if user_input is None: + return await self._show_setup_form(user_input) + + self._organization = user_input[CONF_ORG] + self._project = user_input[CONF_PROJECT] + self._pat = user_input.get(CONF_PAT) + + await self.async_set_unique_id(f"{self._organization}_{self._project}") + self._abort_if_unique_id_configured() + + errors = await self._check_setup() + if errors is not None: + return await self._show_setup_form(errors) + return self._async_create_entry() + + async def async_step_reauth(self, user_input): + """Handle configuration by re-auth.""" + if user_input.get(CONF_ORG) and user_input.get(CONF_PROJECT): + self._organization = user_input[CONF_ORG] + self._project = user_input[CONF_PROJECT] + self._pat = user_input[CONF_PAT] + + self.context["title_placeholders"] = { + "project_url": f"{self._organization}/{self._project}", + } + + await self.async_set_unique_id(f"{self._organization}_{self._project}") + + errors = await self._check_setup() + if errors is not None: + return await self._show_reauth_form(errors) + + entry = await self.async_set_unique_id(self.unique_id) + self.hass.config_entries.async_update_entry( + entry, + data={ + CONF_ORG: self._organization, + CONF_PROJECT: self._project, + CONF_PAT: self._pat, + }, + ) + return self.async_abort(reason="reauth_successful") + + def _async_create_entry(self): + """Handle create entry.""" + return self.async_create_entry( + title=f"{self._organization}/{self._project}", + data={ + CONF_ORG: self._organization, + CONF_PROJECT: self._project, + CONF_PAT: self._pat, + }, + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/const.py new file mode 100644 index 00000000000..40610ba7baa --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/const.py @@ -0,0 +1,11 @@ +"""Constants for the Azure DevOps integration.""" +DOMAIN = "azure_devops" + +DATA_AZURE_DEVOPS_CLIENT = "azure_devops_client" +DATA_ORG = "organization" +DATA_PROJECT = "project" +DATA_PAT = "personal_access_token" + +CONF_ORG = "organization" +CONF_PROJECT = "project" +CONF_PAT = "personal_access_token" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/manifest.json new file mode 100644 index 00000000000..1dd04753293 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "azure_devops", + "name": "Azure DevOps", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/azure_devops", + "requirements": ["aioazuredevops==1.3.5"], + "codeowners": ["@timmo001"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/sensor.py new file mode 100644 index 00000000000..ef6697dea5f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/sensor.py @@ -0,0 +1,150 @@ +"""Support for Azure DevOps sensors.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from aioazuredevops.builds import DevOpsBuild +from aioazuredevops.client import DevOpsClient +import aiohttp + +from homeassistant.components.azure_devops import AzureDevOpsDeviceEntity +from homeassistant.components.azure_devops.const import ( + CONF_ORG, + CONF_PROJECT, + DATA_AZURE_DEVOPS_CLIENT, + DATA_ORG, + DATA_PROJECT, + DOMAIN, +) +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=300) +PARALLEL_UPDATES = 4 + +BUILDS_QUERY = "?queryOrder=queueTimeDescending&maxBuildsPerDefinition=1" + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +) -> None: + """Set up Azure DevOps sensor based on a config entry.""" + instance_key = f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}" + client = hass.data[instance_key][DATA_AZURE_DEVOPS_CLIENT] + organization = entry.data[DATA_ORG] + project = entry.data[DATA_PROJECT] + sensors = [] + + try: + builds: list[DevOpsBuild] = await client.get_builds( + organization, project, BUILDS_QUERY + ) + except aiohttp.ClientError as exception: + _LOGGER.warning(exception) + raise PlatformNotReady from exception + + for build in builds: + sensors.append( + AzureDevOpsLatestBuildSensor(client, organization, project, build) + ) + + async_add_entities(sensors, True) + + +class AzureDevOpsSensor(AzureDevOpsDeviceEntity, SensorEntity): + """Defines a Azure DevOps sensor.""" + + def __init__( + self, + client: DevOpsClient, + organization: str, + project: str, + key: str, + name: str, + icon: str, + measurement: str = "", + unit_of_measurement: str = "", + ) -> None: + """Initialize Azure DevOps sensor.""" + self._state = None + self._attributes = None + self._available = False + self._unit_of_measurement = unit_of_measurement + self.measurement = measurement + self.client = client + self.organization = organization + self.project = project + self.key = key + + super().__init__(organization, project, name, icon) + + @property + def unique_id(self) -> str: + """Return the unique ID for this sensor.""" + return "_".join([self.organization, self.key]) + + @property + def state(self) -> str: + """Return the state of the sensor.""" + return self._state + + @property + def extra_state_attributes(self) -> object: + """Return the attributes of the sensor.""" + return self._attributes + + @property + def unit_of_measurement(self) -> str: + """Return the unit this state is expressed in.""" + return self._unit_of_measurement + + +class AzureDevOpsLatestBuildSensor(AzureDevOpsSensor): + """Defines a Azure DevOps card count sensor.""" + + def __init__( + self, client: DevOpsClient, organization: str, project: str, build: DevOpsBuild + ): + """Initialize Azure DevOps sensor.""" + self.build: DevOpsBuild = build + super().__init__( + client, + organization, + project, + f"{build.project.id}_{build.definition.id}_latest_build", + f"{build.project.name} {build.definition.name} Latest Build", + "mdi:pipe", + ) + + async def _azure_devops_update(self) -> bool: + """Update Azure DevOps entity.""" + try: + build: DevOpsBuild = await self.client.get_build( + self.organization, self.project, self.build.id + ) + except aiohttp.ClientError as exception: + _LOGGER.warning(exception) + self._available = False + return False + self._state = build.build_number + self._attributes = { + "definition_id": build.definition.id, + "definition_name": build.definition.name, + "id": build.id, + "reason": build.reason, + "result": build.result, + "source_branch": build.source_branch, + "source_version": build.source_version, + "status": build.status, + "url": build.links.web, + "queue_time": build.queue_time, + "start_time": build.start_time, + "finish_time": build.finish_time, + } + self._available = True + return True diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/strings.json new file mode 100644 index 00000000000..8dfd203c84b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/strings.json @@ -0,0 +1,32 @@ +{ + "config": { + "flow_title": "{project_url}", + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "project_error": "Could not get project info." + }, + "step": { + "user": { + "data": { + "organization": "Organization", + "project": "Project", + "personal_access_token": "Personal Access Token (PAT)" + }, + "description": "Set up an Azure DevOps instance to access your project. A Personal Access Token is only required for a private project.", + "title": "Add Azure DevOps Project" + }, + "reauth": { + "data": { + "personal_access_token": "Personal Access Token (PAT)" + }, + "description": "Authentication failed for {project_url}. Please enter your current credentials.", + "title": "Reauthentication" + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/ca.json new file mode 100644 index 00000000000..b3eb6e4eb8e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/ca.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja ha estat configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "project_error": "No s'ha pogut obtenir la informaci\u00f3 del projecte." + }, + "flow_title": "{project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "Token d'Acc\u00e9s Personal (PAT)" + }, + "description": "L'autenticaci\u00f3 de {project_url} ha fallat. Si us plau, introdueix les teves credencials actuals.", + "title": "Reautenticaci\u00f3" + }, + "user": { + "data": { + "organization": "Organitzaci\u00f3", + "personal_access_token": "Token d'Acc\u00e9s Personal (PAT)", + "project": "Projecte" + }, + "description": "Configura una inst\u00e0ncia d'Azure DevOps per accedir al teu projecte. El token d'acc\u00e9s personal nom\u00e9s \u00e9s necessari per a projectes privats.", + "title": "Afegeix un projecte Azure DevOps" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/cs.json new file mode 100644 index 00000000000..8ad9968e157 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/cs.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "project_error": "Nelze z\u00edskat informace o projektu." + }, + "flow_title": "Azure DevOps: {project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "Osobn\u00ed p\u0159\u00edstupov\u00fd token (PAT)" + }, + "description": "Ov\u011b\u0159en\u00ed pro {project_url} se nezda\u0159ilo. Zadejte sv\u00e9 aktu\u00e1ln\u00ed p\u0159ihla\u0161ovac\u00ed \u00fadaje.", + "title": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed" + }, + "user": { + "data": { + "organization": "Organizace", + "personal_access_token": "Osobn\u00ed p\u0159\u00edstupov\u00fd token (PAT)", + "project": "Projekt" + }, + "title": "P\u0159idejte projekt Azure DevOps" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/de.json new file mode 100644 index 00000000000..43a5776da2e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/de.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "project_error": "Konnte keine Projektinformationen erhalten." + }, + "flow_title": "Azure DevOps: {project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "Pers\u00f6nlicher Zugriffstoken (PAT)" + }, + "description": "Authentifizierung f\u00fcr {project_url} fehlgeschlagen. Bitte gib deine aktuellen Anmeldedaten ein.", + "title": "Erneute Authentifizierung" + }, + "user": { + "data": { + "organization": "Organisation", + "personal_access_token": "Pers\u00f6nlicher Zugriffstoken (PAT)", + "project": "Projekt" + }, + "description": "Richte eine Azure DevOps-Instanz f\u00fcr den Zugriff auf dein Projekt ein. Ein pers\u00f6nlicher Zugriffstoken ist nur f\u00fcr ein privates Projekt erforderlich.", + "title": "Azure DevOps-Projekt hinzuf\u00fcgen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/en.json new file mode 100644 index 00000000000..66ee72007b9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/en.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "project_error": "Could not get project info." + }, + "flow_title": "{project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "Personal Access Token (PAT)" + }, + "description": "Authentication failed for {project_url}. Please enter your current credentials.", + "title": "Reauthentication" + }, + "user": { + "data": { + "organization": "Organization", + "personal_access_token": "Personal Access Token (PAT)", + "project": "Project" + }, + "description": "Set up an Azure DevOps instance to access your project. A Personal Access Token is only required for a private project.", + "title": "Add Azure DevOps Project" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/es.json new file mode 100644 index 00000000000..1055fdebf41 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/es.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya ha sido configurada", + "reauth_successful": "Token de acceso actualizado correctamente " + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "project_error": "No se pudo obtener informaci\u00f3n del proyecto." + }, + "flow_title": "Azure DevOps: {project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "Token Personal de Acceso (PAT)" + }, + "description": "Error de autenticaci\u00f3n para {project_url}. Por favor, introduce tus credenciales actuales.", + "title": "Reautenticaci\u00f3n" + }, + "user": { + "data": { + "organization": "Organizaci\u00f3n", + "personal_access_token": "Token Personal de Acceso (PAT)", + "project": "Proyecto" + }, + "description": "Configura una instancia de Azure DevOps para acceder a tu proyecto. Un Token Personal de Acceso s\u00f3lo es necesario para un proyecto privado.", + "title": "A\u00f1adir Proyecto Azure DevOps" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/et.json new file mode 100644 index 00000000000..58931e3fd37 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/et.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendus nurjus", + "invalid_auth": "Tuvastamise viga", + "project_error": "Projekti teavet ei \u00f5nnestunud hankida." + }, + "flow_title": "{project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "Isiklik juurdep\u00e4\u00e4suluba (PAT)" + }, + "description": "{project_url} tuvastamine nurjus. Sisesta oma kehtivad andmed.", + "title": "Taastuvastamine" + }, + "user": { + "data": { + "organization": "Organisatsioon", + "personal_access_token": "Isiklik juurdep\u00e4\u00e4suluba (PAT)", + "project": "Projekt" + }, + "description": "Projektile juurdep\u00e4\u00e4semiseks seadista Azure DevOpsi eksemplar. Isiklik juurdep\u00e4\u00e4suluba on vajalik ainult eraprojekti jaoks.", + "title": "Lisa Azure DevOps Project" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/fr.json new file mode 100644 index 00000000000..5e62d54ec1d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/fr.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "Jeton d'acc\u00e8s mis \u00e0 jour avec succ\u00e8s" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "project_error": "Impossible d'obtenir les informations sur le projet." + }, + "flow_title": "Azure DevOps: {project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "Jeton d'acc\u00e8s personnel (PAT)" + }, + "description": "L'authentification a \u00e9chou\u00e9 pour {project_url} . Veuillez saisir vos informations d'identification actuelles.", + "title": "R\u00e9authentification" + }, + "user": { + "data": { + "organization": "Organisation", + "personal_access_token": "Jeton d'acc\u00e8s personnel (PAT)", + "project": "Projet" + }, + "description": "Configurez une instance Azure DevOps pour acc\u00e9der \u00e0 votre projet. Un jeton d'acc\u00e8s personnel n'est requis que pour un projet priv\u00e9.", + "title": "Ajouter un projet Azure DevOps" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/hu.json new file mode 100644 index 00000000000..460b6132048 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "reauth": { + "description": "A(z) {project_url} hiteles\u00edt\u00e9se nem siker\u00fclt. K\u00e9rj\u00fck, adja meg jelenlegi hiteles\u00edt\u0151 adatait." + }, + "user": { + "title": "Azure DevOps Project hozz\u00e1ad\u00e1sa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/id.json new file mode 100644 index 00000000000..42292805b08 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/id.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "project_error": "Tidak bisa mendapatkan info proyek." + }, + "flow_title": "Azure DevOps: {project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "Token Akses Pribadi (PAT)" + }, + "description": "Autentikasi gagal untuk {project_url} . Masukkan kredensial Anda saat ini.", + "title": "Autentikasi ulang" + }, + "user": { + "data": { + "organization": "Organisasi", + "personal_access_token": "Token Akses Pribadi (PAT)", + "project": "Proyek" + }, + "description": "Siapkan instans Azure DevOps untuk mengakses proyek Anda. Token Akses Pribadi hanya diperlukan untuk proyek pribadi.", + "title": "Tambahkan Proyek Azure DevOps" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/it.json new file mode 100644 index 00000000000..02900b93935 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/it.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "project_error": "Non \u00e8 stato possibile ottenere informazioni sul progetto." + }, + "flow_title": "{project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "Token di Accesso Personale (PAT)" + }, + "description": "Autenticazione non riuscita per {project_url}. Si prega di inserire le proprie credenziali attuali.", + "title": "Riautenticazione" + }, + "user": { + "data": { + "organization": "Organizzazione", + "personal_access_token": "Token di Accesso Personale (PAT)", + "project": "Progetto" + }, + "description": "Configurare un'istanza di DevOps di Azure per accedere al progetto. Un Token di Accesso Personale (PAT) \u00e8 richiesto solo per un progetto privato.", + "title": "Aggiungere un progetto Azure DevOps" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/ka.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/ka.json new file mode 100644 index 00000000000..dd48647ed7b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/ka.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0", + "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/ko.json new file mode 100644 index 00000000000..cdb67cf77df --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/ko.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "project_error": "\ud504\ub85c\uc81d\ud2b8 \uc815\ubcf4\ub97c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "flow_title": "Azure DevOps: {project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070 (PAT)" + }, + "description": "{project_url} \uc5d0 \ub300\ud55c \uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. \ud604\uc7ac \uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "\uc7ac\uc778\uc99d" + }, + "user": { + "data": { + "organization": "\uc870\uc9c1", + "personal_access_token": "\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070 (PAT)", + "project": "\ud504\ub85c\uc81d\ud2b8" + }, + "description": "\ud504\ub85c\uc81d\ud2b8\uc5d0 \uc811\uadfc\ud560 Azure DevOps \uc778\uc2a4\ud134\uc2a4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694. \uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070\uc740 \uac1c\uc778 \ud504\ub85c\uc81d\ud2b8\uc5d0\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4.", + "title": "Azure DevOps \ud504\ub85c\uc81d\ud2b8 \ucd94\uac00\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/lb.json new file mode 100644 index 00000000000..d7b7864e7e3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/lb.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Kont ass scho konfigur\u00e9iert", + "reauth_successful": "Re-authentifikatioun war erfollegr\u00e4ich" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "project_error": "Konnt keng Projet Informatiounen ausliesen." + }, + "flow_title": "Azure DevOps: {project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "Pers\u00e9inlechen Acc\u00e8s Jeton (PAT)" + }, + "description": "Feeler bei der Authentifikatioun fir {project_url}. G\u00ebff deng aktuell Umeldungsinformatiounen an.", + "title": "Reauthentifikatioun" + }, + "user": { + "data": { + "organization": "Organisatioun", + "personal_access_token": "Pers\u00e9inlechen Acc\u00e8s Jeton (PAT)", + "project": "Projet" + }, + "description": "Riicht eng Azure DevOps Instanz an fir d\u00e4in Projet z'acc\u00e9d\u00e9ieren. E Pers\u00e9inlechen Acc\u00e8s Jetons ass n\u00ebmme fir ee Private Projet n\u00e9ideg.", + "title": "Azure DevOps Project dob\u00e4isetzen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/nl.json new file mode 100644 index 00000000000..a57dd85c495 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/nl.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "project_error": "Kon geen projectinformatie ophalen." + }, + "flow_title": "{project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "Persoonlijk toegangstoken (PAT)" + }, + "description": "Authenticatie mislukt voor {project_url}. Voer uw huidige inloggegevens in.", + "title": "Herauthenticatie" + }, + "user": { + "data": { + "organization": "Organisatie", + "personal_access_token": "Persoonlijk toegangstoken (PAT)", + "project": "Project" + }, + "description": "Stel een Azure DevOps instantie in om toegang te krijgen tot uw project. Een persoonlijke toegangstoken is alleen nodig voor een priv\u00e9project.", + "title": "Azure DevOps-project toevoegen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/no.json new file mode 100644 index 00000000000..ba4ff946595 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/no.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "project_error": "Kunne ikke f\u00e5 prosjektinformasjon." + }, + "flow_title": "{project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "Personlig tilgangstoken (PAT)" + }, + "description": "Autentiseringen mislyktes for {project_url}. Vennligst skriv inn gjeldende legitimasjon.", + "title": "Godkjenne integrering p\u00e5 nytt" + }, + "user": { + "data": { + "organization": "Organisasjon", + "personal_access_token": "Token for personlig tilgang (PAT)", + "project": "Prosjekt" + }, + "description": "Sett opp en Azure DevOps-forekomst for \u00e5 f\u00e5 tilgang til prosjektet ditt. En personlig tilgangstoken er bare n\u00f8dvendig for et privat prosjekt.", + "title": "Legg til Azure DevOps Project" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/pl.json new file mode 100644 index 00000000000..e285af979bd --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/pl.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "project_error": "Nie mo\u017cna uzyska\u0107 informacji o projekcie" + }, + "flow_title": "{project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "Osobisty token dost\u0119pu (PAT)" + }, + "description": "Uwierzytelnianie dla {project_url} nie powiod\u0142o si\u0119. Wprowad\u017a aktualne dane uwierzytelniaj\u0105ce.", + "title": "Ponowne uwierzytelnianie" + }, + "user": { + "data": { + "organization": "Organizacja", + "personal_access_token": "Osobisty token dost\u0119pu (PAT)", + "project": "Projekt" + }, + "description": "Skonfiguruj instancj\u0119 Azure DevOps, aby uzyska\u0107 dost\u0119p do swojego projektu. Osobisty token dost\u0119pu (PAT) jest wymagany tylko dla prywatnego projektu.", + "title": "Dodaj projekt Azure DevOps" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/pt-BR.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/pt-BR.json new file mode 100644 index 00000000000..510159829cb --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "project_error": "N\u00e3o foi poss\u00edvel obter informa\u00e7\u00f5es do projeto." + }, + "step": { + "reauth": { + "data": { + "personal_access_token": "Token de acesso pessoal (PAT)" + }, + "description": "A autentica\u00e7\u00e3o falhou para {project_url}. Por favor, insira suas credenciais atuais.", + "title": "Reautentica\u00e7\u00e3o" + }, + "user": { + "data": { + "organization": "Organiza\u00e7\u00e3o", + "personal_access_token": "Token de acesso pessoal (PAT)", + "project": "Projeto" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/pt.json new file mode 100644 index 00000000000..2af1f548447 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "reauth_successful": "Token de Acesso atualizado com sucesso" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/ru.json new file mode 100644 index 00000000000..5e629b6d558 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/ru.json @@ -0,0 +1,32 @@ +{ + "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_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": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "project_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0435." + }, + "flow_title": "{project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "\u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 (PAT)" + }, + "description": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 {project_url}. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "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": { + "organization": "\u041e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044f", + "personal_access_token": "\u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 (PAT)", + "project": "\u041f\u0440\u043e\u0435\u043a\u0442" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u043c Azure DevOps. \u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0447\u0430\u0441\u0442\u043d\u044b\u0445 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0432.", + "title": "Azure DevOps" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/tr.json new file mode 100644 index 00000000000..11a15956f63 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/tr.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "project_error": "Proje bilgileri al\u0131namad\u0131." + }, + "flow_title": "Azure DevOps: {project_url}", + "step": { + "reauth": { + "title": "Yeniden kimlik do\u011frulama" + }, + "user": { + "data": { + "organization": "Organizasyon", + "personal_access_token": "Ki\u015fisel Eri\u015fim Belirteci (PAT)", + "project": "Proje" + }, + "description": "Projenize eri\u015fmek i\u00e7in bir Azure DevOps \u00f6rne\u011fi ayarlay\u0131n. Ki\u015fisel Eri\u015fim Jetonu yaln\u0131zca \u00f6zel bir proje i\u00e7in gereklidir.", + "title": "Azure DevOps Projesi Ekle" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/uk.json new file mode 100644 index 00000000000..848528f444e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/uk.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "project_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u043f\u0440\u043e\u0435\u043a\u0442." + }, + "flow_title": "Azure DevOps: {project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "\u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 (PAT)" + }, + "description": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457 {project_url} . \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + }, + "user": { + "data": { + "organization": "\u041e\u0440\u0433\u0430\u043d\u0456\u0437\u0430\u0446\u0456\u044f", + "personal_access_token": "\u0422\u043e\u043a\u0435\u043d \u043e\u0441\u043e\u0431\u0438\u0441\u0442\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0443 (PAT)", + "project": "\u041f\u0440\u043e\u0435\u043a\u0442" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u043c Azure DevOps. \u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0432\u0432\u043e\u0434\u0438\u0442\u0438 \u043b\u0438\u0448\u0435 \u0434\u043b\u044f \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u0438\u0445 \u043f\u0440\u043e\u0435\u043a\u0442\u0456\u0432.", + "title": "\u0414\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u043e\u0435\u043a\u0442 Azure DevOps" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/zh-Hans.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/zh-Hans.json new file mode 100644 index 00000000000..b0c629646e2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/zh-Hans.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u9a8c\u8bc1\u7801\u65e0\u6548" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/zh-Hant.json new file mode 100644 index 00000000000..13f6fcbe276 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_devops/translations/zh-Hant.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "project_error": "\u7121\u6cd5\u53d6\u5f97\u5c08\u6848\u8cc7\u8a0a\u3002" + }, + "flow_title": "{project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "\u500b\u4eba\u5b58\u53d6\u5bc6\u9470\uff08PAT\uff09" + }, + "description": "{project_url}\u8a8d\u8b49\u5931\u6557\u3002\u8acb\u8f38\u5165\u76ee\u524d\u8b49\u66f8\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49" + }, + "user": { + "data": { + "organization": "\u7d44\u7e54", + "personal_access_token": "\u500b\u4eba\u5b58\u53d6\u5bc6\u9470\uff08PAT\uff09", + "project": "\u5c08\u6848" + }, + "description": "\u8a2d\u5b9a Azure DevOps \u4ee5\u5b58\u53d6\u5c08\u6848\u3002\u79c1\u4eba\u5c08\u6848\u5247\u9700\u8981\u8f38\u5165\u300c\u500b\u4eba\u5b58\u53d6\u5bc6\u9470\uff09\u3002", + "title": "\u65b0\u589e Azure DevOps \u5c08\u6848" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_event_hub/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_event_hub/__init__.py new file mode 100644 index 00000000000..0473c4ff5a7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_event_hub/__init__.py @@ -0,0 +1,225 @@ +"""Support for Azure Event Hubs.""" +from __future__ import annotations + +import asyncio +import json +import logging +import time +from typing import Any + +from azure.eventhub import EventData +from azure.eventhub.aio import EventHubProducerClient, EventHubSharedKeyCredential +from azure.eventhub.exceptions import EventHubError +import voluptuous as vol + +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, + MATCH_ALL, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import Event, HomeAssistant +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entityfilter import FILTER_SCHEMA +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.json import JSONEncoder + +from .const import ( + ADDITIONAL_ARGS, + CONF_EVENT_HUB_CON_STRING, + CONF_EVENT_HUB_INSTANCE_NAME, + CONF_EVENT_HUB_NAMESPACE, + CONF_EVENT_HUB_SAS_KEY, + CONF_EVENT_HUB_SAS_POLICY, + CONF_FILTER, + CONF_MAX_DELAY, + CONF_SEND_INTERVAL, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Exclusive(CONF_EVENT_HUB_CON_STRING, "setup_methods"): cv.string, + vol.Exclusive(CONF_EVENT_HUB_NAMESPACE, "setup_methods"): cv.string, + vol.Optional(CONF_EVENT_HUB_INSTANCE_NAME): cv.string, + vol.Optional(CONF_EVENT_HUB_SAS_POLICY): cv.string, + vol.Optional(CONF_EVENT_HUB_SAS_KEY): cv.string, + vol.Optional(CONF_SEND_INTERVAL, default=5): cv.positive_int, + vol.Optional(CONF_MAX_DELAY, default=30): cv.positive_int, + vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, + }, + cv.has_at_least_one_key( + CONF_EVENT_HUB_CON_STRING, CONF_EVENT_HUB_NAMESPACE + ), + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, yaml_config): + """Activate Azure EH component.""" + config = yaml_config[DOMAIN] + if config.get(CONF_EVENT_HUB_CON_STRING): + client_args = {"conn_str": config[CONF_EVENT_HUB_CON_STRING]} + conn_str_client = True + else: + client_args = { + "fully_qualified_namespace": f"{config[CONF_EVENT_HUB_NAMESPACE]}.servicebus.windows.net", + "credential": EventHubSharedKeyCredential( + policy=config[CONF_EVENT_HUB_SAS_POLICY], + key=config[CONF_EVENT_HUB_SAS_KEY], + ), + "eventhub_name": config[CONF_EVENT_HUB_INSTANCE_NAME], + } + conn_str_client = False + + instance = hass.data[DOMAIN] = AzureEventHub( + hass, + client_args, + conn_str_client, + config[CONF_FILTER], + config[CONF_SEND_INTERVAL], + config[CONF_MAX_DELAY], + ) + + hass.async_create_task(instance.async_start()) + return True + + +class AzureEventHub: + """A event handler class for Azure Event Hub.""" + + def __init__( + self, + hass: HomeAssistant, + client_args: dict[str, Any], + conn_str_client: bool, + entities_filter: vol.Schema, + send_interval: int, + max_delay: int, + ): + """Initialize the listener.""" + self.hass = hass + self.queue = asyncio.PriorityQueue() + self._client_args = client_args + self._conn_str_client = conn_str_client + self._entities_filter = entities_filter + self._send_interval = send_interval + self._max_delay = max_delay + send_interval + self._listener_remover = None + self._next_send_remover = None + self.shutdown = False + + async def async_start(self): + """Start the recorder, suppress logging and register the callbacks and do the first send after five seconds, to capture the startup events.""" + # suppress the INFO and below logging on the underlying packages, they are very verbose, even at INFO + logging.getLogger("uamqp").setLevel(logging.WARNING) + logging.getLogger("azure.eventhub").setLevel(logging.WARNING) + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_shutdown) + self._listener_remover = self.hass.bus.async_listen( + MATCH_ALL, self.async_listen + ) + # schedule the first send after 10 seconds to capture startup events, after that each send will schedule the next after the interval. + self._next_send_remover = async_call_later(self.hass, 10, self.async_send) + + async def async_shutdown(self, _: Event): + """Shut down the AEH by queueing None and calling send.""" + if self._next_send_remover: + self._next_send_remover() + if self._listener_remover: + self._listener_remover() + await self.queue.put((3, (time.monotonic(), None))) + await self.async_send(None) + + async def async_listen(self, event: Event): + """Listen for new messages on the bus and queue them for AEH.""" + await self.queue.put((2, (time.monotonic(), event))) + + async def async_send(self, _): + """Write preprocessed events to eventhub, with retry.""" + client = self._get_client() + async with client: + while not self.queue.empty(): + data_batch, dequeue_count = await self.fill_batch(client) + _LOGGER.debug( + "Sending %d event(s), out of %d events in the queue", + len(data_batch), + dequeue_count, + ) + if data_batch: + try: + await client.send_batch(data_batch) + except EventHubError as exc: + _LOGGER.error("Error in sending events to Event Hub: %s", exc) + finally: + for _ in range(dequeue_count): + self.queue.task_done() + await client.close() + + if not self.shutdown: + self._next_send_remover = async_call_later( + self.hass, self._send_interval, self.async_send + ) + + async def fill_batch(self, client): + """Return a batch of events formatted for writing. + + Uses get_nowait instead of await get, because the functions batches and doesn't wait for each single event, the send function is called. + + Throws ValueError on add to batch when the EventDataBatch object reaches max_size. Put the item back in the queue and the next batch will include it. + """ + event_batch = await client.create_batch() + dequeue_count = 0 + dropped = 0 + while not self.shutdown: + try: + _, (timestamp, event) = self.queue.get_nowait() + except asyncio.QueueEmpty: + break + dequeue_count += 1 + if not event: + self.shutdown = True + break + event_data = self._event_to_filtered_event_data(event) + if not event_data: + continue + if time.monotonic() - timestamp <= self._max_delay: + try: + event_batch.add(event_data) + except ValueError: + self.queue.put_nowait((1, (timestamp, event))) + break + else: + dropped += 1 + + if dropped: + _LOGGER.warning( + "Dropped %d old events, consider increasing the max_delay", dropped + ) + + return event_batch, dequeue_count + + def _event_to_filtered_event_data(self, event: Event): + """Filter event states and create EventData object.""" + state = event.data.get("new_state") + if ( + state is None + or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE) + or not self._entities_filter(state.entity_id) + ): + return None + return EventData(json.dumps(obj=state, cls=JSONEncoder).encode("utf-8")) + + def _get_client(self): + """Get a Event Producer Client.""" + if self._conn_str_client: + return EventHubProducerClient.from_connection_string( + **self._client_args, **ADDITIONAL_ARGS + ) + return EventHubProducerClient(**self._client_args, **ADDITIONAL_ARGS) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_event_hub/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_event_hub/const.py new file mode 100644 index 00000000000..1786bb5cbf2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_event_hub/const.py @@ -0,0 +1,13 @@ +"""Constants and shared schema for the Azure Event Hub integration.""" +DOMAIN = "azure_event_hub" + +CONF_EVENT_HUB_NAMESPACE = "event_hub_namespace" +CONF_EVENT_HUB_INSTANCE_NAME = "event_hub_instance_name" +CONF_EVENT_HUB_SAS_POLICY = "event_hub_sas_policy" +CONF_EVENT_HUB_SAS_KEY = "event_hub_sas_key" +CONF_EVENT_HUB_CON_STRING = "event_hub_connection_string" +CONF_SEND_INTERVAL = "send_interval" +CONF_MAX_DELAY = "max_delay" +CONF_FILTER = "filter" + +ADDITIONAL_ARGS = {"logging_enable": False} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_event_hub/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_event_hub/manifest.json new file mode 100644 index 00000000000..b570f11e28f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_event_hub/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "azure_event_hub", + "name": "Azure Event Hub", + "documentation": "https://www.home-assistant.io/integrations/azure_event_hub", + "requirements": ["azure-eventhub==5.1.0"], + "codeowners": ["@eavanvalkenburg"], + "iot_class": "cloud_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_service_bus/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_service_bus/__init__.py new file mode 100644 index 00000000000..f18dc9eb66c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_service_bus/__init__.py @@ -0,0 +1 @@ +"""The Azure Service Bus integration.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_service_bus/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_service_bus/manifest.json new file mode 100644 index 00000000000..5de15056b08 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_service_bus/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "azure_service_bus", + "name": "Azure Service Bus", + "documentation": "https://www.home-assistant.io/integrations/azure_service_bus", + "requirements": ["azure-servicebus==0.50.3"], + "codeowners": ["@hfurubotten"], + "iot_class": "cloud_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/azure_service_bus/notify.py b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_service_bus/notify.py new file mode 100644 index 00000000000..e7c85adede8 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/azure_service_bus/notify.py @@ -0,0 +1,106 @@ +"""Support for azure service bus notification.""" +import json +import logging + +from azure.servicebus.aio import Message, ServiceBusClient +from azure.servicebus.common.errors import ( + MessageSendFailed, + ServiceBusConnectionError, + ServiceBusResourceNotFound, +) +import voluptuous as vol + +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_TARGET, + ATTR_TITLE, + PLATFORM_SCHEMA, + BaseNotificationService, +) +from homeassistant.const import CONTENT_TYPE_JSON +import homeassistant.helpers.config_validation as cv + +CONF_CONNECTION_STRING = "connection_string" +CONF_QUEUE_NAME = "queue" +CONF_TOPIC_NAME = "topic" + +ATTR_ASB_MESSAGE = "message" +ATTR_ASB_TITLE = "title" +ATTR_ASB_TARGET = "target" + +PLATFORM_SCHEMA = vol.All( + cv.has_at_least_one_key(CONF_QUEUE_NAME, CONF_TOPIC_NAME), + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_CONNECTION_STRING): cv.string, + vol.Exclusive( + CONF_QUEUE_NAME, "output", "Can only send to a queue or a topic." + ): cv.string, + vol.Exclusive( + CONF_TOPIC_NAME, "output", "Can only send to a queue or a topic." + ): cv.string, + } + ), +) + +_LOGGER = logging.getLogger(__name__) + + +def get_service(hass, config, discovery_info=None): + """Get the notification service.""" + connection_string = config[CONF_CONNECTION_STRING] + queue_name = config.get(CONF_QUEUE_NAME) + topic_name = config.get(CONF_TOPIC_NAME) + + # Library can do synchronous IO when creating the clients. + # Passes in loop here, but can't run setup on the event loop. + servicebus = ServiceBusClient.from_connection_string( + connection_string, loop=hass.loop + ) + + try: + if queue_name: + client = servicebus.get_queue(queue_name) + else: + client = servicebus.get_topic(topic_name) + except (ServiceBusConnectionError, ServiceBusResourceNotFound) as err: + _LOGGER.error( + "Connection error while creating client for queue/topic '%s'. %s", + queue_name or topic_name, + err, + ) + return None + + return ServiceBusNotificationService(client) + + +class ServiceBusNotificationService(BaseNotificationService): + """Implement the notification service for the service bus service.""" + + def __init__(self, client): + """Initialize the service.""" + self._client = client + + async def async_send_message(self, message, **kwargs): + """Send a message.""" + dto = {ATTR_ASB_MESSAGE: message} + + if ATTR_TITLE in kwargs: + dto[ATTR_ASB_TITLE] = kwargs[ATTR_TITLE] + if ATTR_TARGET in kwargs: + dto[ATTR_ASB_TARGET] = kwargs[ATTR_TARGET] + + data = kwargs.get(ATTR_DATA) + if data: + dto.update(data) + + queue_message = Message(json.dumps(dto)) + queue_message.properties.content_type = CONTENT_TYPE_JSON + try: + await self._client.send(queue_message) + except MessageSendFailed as err: + _LOGGER.error( + "Could not send service bus notification to %s. %s", + self._client.name, + err, + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/baidu/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/baidu/__init__.py new file mode 100644 index 00000000000..8a332cf52e1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/baidu/__init__.py @@ -0,0 +1 @@ +"""Support for Baidu integration.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/baidu/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/baidu/manifest.json new file mode 100644 index 00000000000..e808da42728 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/baidu/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "baidu", + "name": "Baidu", + "documentation": "https://www.home-assistant.io/integrations/baidu", + "requirements": ["baidu-aip==1.6.6"], + "codeowners": [], + "iot_class": "cloud_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/baidu/tts.py b/homeassistant-2021.6.0.dev0/homeassistant/components/baidu/tts.py new file mode 100644 index 00000000000..72694248fa1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/baidu/tts.py @@ -0,0 +1,134 @@ +"""Support for Baidu speech service.""" +import logging + +from aip import AipSpeech +import voluptuous as vol + +from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider +from homeassistant.const import CONF_API_KEY +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +SUPPORTED_LANGUAGES = ["zh"] +DEFAULT_LANG = "zh" +SUPPORTED_PERSON = [0, 1, 3, 4, 5, 103, 106, 110, 111, 5003, 5118] + +CONF_APP_ID = "app_id" +CONF_SECRET_KEY = "secret_key" +CONF_SPEED = "speed" +CONF_PITCH = "pitch" +CONF_VOLUME = "volume" +CONF_PERSON = "person" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORTED_LANGUAGES), + vol.Required(CONF_APP_ID): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_SECRET_KEY): cv.string, + vol.Optional(CONF_SPEED, default=5): vol.All( + vol.Coerce(int), vol.Range(min=0, max=9) + ), + vol.Optional(CONF_PITCH, default=5): vol.All( + vol.Coerce(int), vol.Range(min=0, max=9) + ), + vol.Optional(CONF_VOLUME, default=5): vol.All( + vol.Coerce(int), vol.Range(min=0, max=15) + ), + vol.Optional(CONF_PERSON, default=0): vol.In(SUPPORTED_PERSON), + } +) + +# Keys are options in the config file, and Values are options +# required by Baidu TTS API. +_OPTIONS = { + CONF_PERSON: "per", + CONF_PITCH: "pit", + CONF_SPEED: "spd", + CONF_VOLUME: "vol", +} +SUPPORTED_OPTIONS = [CONF_PERSON, CONF_PITCH, CONF_SPEED, CONF_VOLUME] + + +def get_engine(hass, config, discovery_info=None): + """Set up Baidu TTS component.""" + return BaiduTTSProvider(hass, config) + + +class BaiduTTSProvider(Provider): + """Baidu TTS speech api provider.""" + + def __init__(self, hass, conf): + """Init Baidu TTS service.""" + self.hass = hass + self._lang = conf[CONF_LANG] + self._codec = "mp3" + self.name = "BaiduTTS" + + self._app_data = { + "appid": conf[CONF_APP_ID], + "apikey": conf[CONF_API_KEY], + "secretkey": conf[CONF_SECRET_KEY], + } + + self._speech_conf_data = { + _OPTIONS[CONF_PERSON]: conf[CONF_PERSON], + _OPTIONS[CONF_PITCH]: conf[CONF_PITCH], + _OPTIONS[CONF_SPEED]: conf[CONF_SPEED], + _OPTIONS[CONF_VOLUME]: conf[CONF_VOLUME], + } + + @property + def default_language(self): + """Return the default language.""" + return self._lang + + @property + def supported_languages(self): + """Return a list of supported languages.""" + return SUPPORTED_LANGUAGES + + @property + def default_options(self): + """Return a dict including default options.""" + return { + CONF_PERSON: self._speech_conf_data[_OPTIONS[CONF_PERSON]], + CONF_PITCH: self._speech_conf_data[_OPTIONS[CONF_PITCH]], + CONF_SPEED: self._speech_conf_data[_OPTIONS[CONF_SPEED]], + CONF_VOLUME: self._speech_conf_data[_OPTIONS[CONF_VOLUME]], + } + + @property + def supported_options(self): + """Return a list of supported options.""" + return SUPPORTED_OPTIONS + + def get_tts_audio(self, message, language, options=None): + """Load TTS from BaiduTTS.""" + + aip_speech = AipSpeech( + self._app_data["appid"], + self._app_data["apikey"], + self._app_data["secretkey"], + ) + + if options is None: + result = aip_speech.synthesis(message, language, 1, self._speech_conf_data) + else: + speech_data = self._speech_conf_data.copy() + for key, value in options.items(): + speech_data[_OPTIONS[key]] = value + + result = aip_speech.synthesis(message, language, 1, speech_data) + + if isinstance(result, dict): + _LOGGER.error( + "Baidu TTS error-- err_no:%d; err_msg:%s; err_detail:%s", + result["err_no"], + result["err_msg"], + result["err_detail"], + ) + return None, None + + return self._codec, result diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/bayesian/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/bayesian/__init__.py new file mode 100644 index 00000000000..485592dc5e4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/bayesian/__init__.py @@ -0,0 +1,4 @@ +"""The bayesian component.""" + +DOMAIN = "bayesian" +PLATFORMS = ["binary_sensor"] diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/bayesian/binary_sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/bayesian/binary_sensor.py new file mode 100644 index 00000000000..6879e278bab --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/bayesian/binary_sensor.py @@ -0,0 +1,418 @@ +"""Use Bayesian Inference to trigger a binary sensor.""" +from collections import OrderedDict +import logging + +import voluptuous as vol + +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.const import ( + CONF_ABOVE, + CONF_BELOW, + CONF_DEVICE_CLASS, + CONF_ENTITY_ID, + CONF_NAME, + CONF_PLATFORM, + CONF_STATE, + CONF_VALUE_TEMPLATE, + STATE_UNKNOWN, +) +from homeassistant.core import callback +from homeassistant.exceptions import ConditionError, TemplateError +from homeassistant.helpers import condition +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import ( + TrackTemplate, + async_track_state_change_event, + async_track_template_result, +) +from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.template import result_as_boolean + +from . import DOMAIN, PLATFORMS + +ATTR_OBSERVATIONS = "observations" +ATTR_OCCURRED_OBSERVATION_ENTITIES = "occurred_observation_entities" +ATTR_PROBABILITY = "probability" +ATTR_PROBABILITY_THRESHOLD = "probability_threshold" + +CONF_OBSERVATIONS = "observations" +CONF_PRIOR = "prior" +CONF_TEMPLATE = "template" +CONF_PROBABILITY_THRESHOLD = "probability_threshold" +CONF_P_GIVEN_F = "prob_given_false" +CONF_P_GIVEN_T = "prob_given_true" +CONF_TO_STATE = "to_state" + +DEFAULT_NAME = "Bayesian Binary Sensor" +DEFAULT_PROBABILITY_THRESHOLD = 0.5 + +_LOGGER = logging.getLogger(__name__) + + +NUMERIC_STATE_SCHEMA = vol.Schema( + { + CONF_PLATFORM: "numeric_state", + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Optional(CONF_ABOVE): vol.Coerce(float), + vol.Optional(CONF_BELOW): vol.Coerce(float), + vol.Required(CONF_P_GIVEN_T): vol.Coerce(float), + vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float), + }, + required=True, +) + +STATE_SCHEMA = vol.Schema( + { + CONF_PLATFORM: CONF_STATE, + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TO_STATE): cv.string, + vol.Required(CONF_P_GIVEN_T): vol.Coerce(float), + vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float), + }, + required=True, +) + +TEMPLATE_SCHEMA = vol.Schema( + { + CONF_PLATFORM: CONF_TEMPLATE, + vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Required(CONF_P_GIVEN_T): vol.Coerce(float), + vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float), + }, + required=True, +) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_DEVICE_CLASS): cv.string, + vol.Required(CONF_OBSERVATIONS): vol.Schema( + vol.All( + cv.ensure_list, + [vol.Any(NUMERIC_STATE_SCHEMA, STATE_SCHEMA, TEMPLATE_SCHEMA)], + ) + ), + vol.Required(CONF_PRIOR): vol.Coerce(float), + vol.Optional( + CONF_PROBABILITY_THRESHOLD, default=DEFAULT_PROBABILITY_THRESHOLD + ): vol.Coerce(float), + } +) + + +def update_probability(prior, prob_given_true, prob_given_false): + """Update probability using Bayes' rule.""" + numerator = prob_given_true * prior + denominator = numerator + prob_given_false * (1 - prior) + return numerator / denominator + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Bayesian Binary sensor.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + + name = config[CONF_NAME] + observations = config[CONF_OBSERVATIONS] + prior = config[CONF_PRIOR] + probability_threshold = config[CONF_PROBABILITY_THRESHOLD] + device_class = config.get(CONF_DEVICE_CLASS) + + async_add_entities( + [ + BayesianBinarySensor( + name, prior, observations, probability_threshold, device_class + ) + ] + ) + + +class BayesianBinarySensor(BinarySensorEntity): + """Representation of a Bayesian sensor.""" + + def __init__(self, name, prior, observations, probability_threshold, device_class): + """Initialize the Bayesian sensor.""" + self._name = name + self._observations = observations + self._probability_threshold = probability_threshold + self._device_class = device_class + self._deviation = False + self._callbacks = [] + + self.prior = prior + self.probability = prior + + self.current_observations = OrderedDict({}) + + self.observations_by_entity = self._build_observations_by_entity() + self.observations_by_template = self._build_observations_by_template() + + self.observation_handlers = { + "numeric_state": self._process_numeric_state, + "state": self._process_state, + } + + async def async_added_to_hass(self): + """ + Call when entity about to be added. + + All relevant update logic for instance attributes occurs within this closure. + Other methods in this class are designed to avoid directly modifying instance + attributes, by instead focusing on returning relevant data back to this method. + + The goal of this method is to ensure that `self.current_observations` and `self.probability` + are set on a best-effort basis when this entity is register with hass. + + In addition, this method must register the state listener defined within, which + will be called any time a relevant entity changes its state. + """ + + @callback + def async_threshold_sensor_state_listener(event): + """ + Handle sensor state changes. + + When a state changes, we must update our list of current observations, + then calculate the new probability. + """ + new_state = event.data.get("new_state") + + if new_state is None or new_state.state == STATE_UNKNOWN: + return + + entity = event.data.get("entity_id") + + self.current_observations.update(self._record_entity_observations(entity)) + self.async_set_context(event.context) + self._recalculate_and_write_state() + + self.async_on_remove( + async_track_state_change_event( + self.hass, + list(self.observations_by_entity), + async_threshold_sensor_state_listener, + ) + ) + + @callback + def _async_template_result_changed(event, updates): + track_template_result = updates.pop() + template = track_template_result.template + result = track_template_result.result + entity = event and event.data.get("entity_id") + + if isinstance(result, TemplateError): + _LOGGER.error( + "TemplateError('%s') " + "while processing template '%s' " + "in entity '%s'", + result, + template, + self.entity_id, + ) + + should_trigger = False + else: + should_trigger = result_as_boolean(result) + + for obs in self.observations_by_template[template]: + if should_trigger: + obs_entry = {"entity_id": entity, **obs} + else: + obs_entry = None + self.current_observations[obs["id"]] = obs_entry + + if event: + self.async_set_context(event.context) + self._recalculate_and_write_state() + + for template in self.observations_by_template: + info = async_track_template_result( + self.hass, + [TrackTemplate(template, None)], + _async_template_result_changed, + ) + + self._callbacks.append(info) + self.async_on_remove(info.async_remove) + info.async_refresh() + + self.current_observations.update(self._initialize_current_observations()) + self.probability = self._calculate_new_probability() + self._deviation = bool(self.probability >= self._probability_threshold) + + @callback + def _recalculate_and_write_state(self): + self.probability = self._calculate_new_probability() + self._deviation = bool(self.probability >= self._probability_threshold) + self.async_write_ha_state() + + def _initialize_current_observations(self): + local_observations = OrderedDict({}) + for entity in self.observations_by_entity: + local_observations.update(self._record_entity_observations(entity)) + return local_observations + + def _record_entity_observations(self, entity): + local_observations = OrderedDict({}) + + for entity_obs in self.observations_by_entity[entity]: + platform = entity_obs["platform"] + + should_trigger = self.observation_handlers[platform](entity_obs) + + if should_trigger: + obs_entry = {"entity_id": entity, **entity_obs} + else: + obs_entry = None + + local_observations[entity_obs["id"]] = obs_entry + + return local_observations + + def _calculate_new_probability(self): + prior = self.prior + + for obs in self.current_observations.values(): + if obs is not None: + prior = update_probability( + prior, + obs["prob_given_true"], + obs.get("prob_given_false", 1 - obs["prob_given_true"]), + ) + + return prior + + def _build_observations_by_entity(self): + """ + Build and return data structure of the form below. + + { + "sensor.sensor1": [{"id": 0, ...}, {"id": 1, ...}], + "sensor.sensor2": [{"id": 2, ...}], + ... + } + + Each "observation" must be recognized uniquely, and it should be possible + for all relevant observations to be looked up via their `entity_id`. + """ + + observations_by_entity = {} + for ind, obs in enumerate(self._observations): + obs["id"] = ind + + if "entity_id" not in obs: + continue + + entity_ids = [obs["entity_id"]] + + for e_id in entity_ids: + observations_by_entity.setdefault(e_id, []).append(obs) + + return observations_by_entity + + def _build_observations_by_template(self): + """ + Build and return data structure of the form below. + + { + "template": [{"id": 0, ...}, {"id": 1, ...}], + "template2": [{"id": 2, ...}], + ... + } + + Each "observation" must be recognized uniquely, and it should be possible + for all relevant observations to be looked up via their `template`. + """ + + observations_by_template = {} + for ind, obs in enumerate(self._observations): + obs["id"] = ind + + if "value_template" not in obs: + continue + + template = obs.get(CONF_VALUE_TEMPLATE) + observations_by_template.setdefault(template, []).append(obs) + + return observations_by_template + + def _process_numeric_state(self, entity_observation): + """Return True if numeric condition is met.""" + entity = entity_observation["entity_id"] + + try: + return condition.async_numeric_state( + self.hass, + entity, + entity_observation.get("below"), + entity_observation.get("above"), + None, + entity_observation, + ) + except ConditionError: + return False + + def _process_state(self, entity_observation): + """Return True if state conditions are met.""" + entity = entity_observation["entity_id"] + + try: + return condition.state( + self.hass, entity, entity_observation.get("to_state") + ) + except ConditionError: + return False + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._deviation + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def device_class(self): + """Return the sensor class of the sensor.""" + return self._device_class + + @property + def extra_state_attributes(self): + """Return the state attributes of the sensor.""" + + attr_observations_list = [ + obs.copy() for obs in self.current_observations.values() if obs is not None + ] + + for item in attr_observations_list: + item.pop("value_template", None) + + return { + ATTR_OBSERVATIONS: attr_observations_list, + ATTR_OCCURRED_OBSERVATION_ENTITIES: list( + { + obs.get("entity_id") + for obs in self.current_observations.values() + if obs is not None and obs.get("entity_id") is not None + } + ), + ATTR_PROBABILITY: round(self.probability, 2), + ATTR_PROBABILITY_THRESHOLD: self._probability_threshold, + } + + async def async_update(self): + """Get the latest data and update the states.""" + if not self._callbacks: + self._recalculate_and_write_state() + return + # Force recalc of the templates. The states will + # update automatically. + for call in self._callbacks: + call.async_refresh() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/bayesian/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/bayesian/manifest.json new file mode 100644 index 00000000000..6a84beb1df6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/bayesian/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "bayesian", + "name": "Bayesian", + "documentation": "https://www.home-assistant.io/integrations/bayesian", + "codeowners": [], + "quality_scale": "internal", + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/bayesian/services.yaml b/homeassistant-2021.6.0.dev0/homeassistant/components/bayesian/services.yaml new file mode 100644 index 00000000000..c1dc891805a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/bayesian/services.yaml @@ -0,0 +1,3 @@ +reload: + name: Reload + description: Reload all bayesian entities diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/bbb_gpio/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/bbb_gpio/__init__.py new file mode 100644 index 00000000000..f7d146e073e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/bbb_gpio/__init__.py @@ -0,0 +1,51 @@ +"""Support for controlling GPIO pins of a Beaglebone Black.""" +from Adafruit_BBIO import GPIO # pylint: disable=import-error + +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP + +DOMAIN = "bbb_gpio" + + +def setup(hass, config): + """Set up the BeagleBone Black GPIO component.""" + + def cleanup_gpio(event): + """Stuff to do before stopping.""" + GPIO.cleanup() + + def prepare_gpio(event): + """Stuff to do when Home Assistant starts.""" + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) + return True + + +def setup_output(pin): + """Set up a GPIO as output.""" + + GPIO.setup(pin, GPIO.OUT) + + +def setup_input(pin, pull_mode): + """Set up a GPIO as input.""" + + GPIO.setup(pin, GPIO.IN, GPIO.PUD_DOWN if pull_mode == "DOWN" else GPIO.PUD_UP) + + +def write_output(pin, value): + """Write a value to a GPIO.""" + + GPIO.output(pin, value) + + +def read_input(pin): + """Read a value from a GPIO.""" + + return GPIO.input(pin) is GPIO.HIGH + + +def edge_detect(pin, event_callback, bounce): + """Add detection for RISING and FALLING events.""" + + GPIO.add_event_detect(pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/bbb_gpio/binary_sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/bbb_gpio/binary_sensor.py new file mode 100644 index 00000000000..c772cf86f00 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/bbb_gpio/binary_sensor.py @@ -0,0 +1,77 @@ +"""Support for binary sensor using Beaglebone Black GPIO.""" +import voluptuous as vol + +from homeassistant.components import bbb_gpio +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME +import homeassistant.helpers.config_validation as cv + +CONF_PINS = "pins" +CONF_BOUNCETIME = "bouncetime" +CONF_INVERT_LOGIC = "invert_logic" +CONF_PULL_MODE = "pull_mode" + +DEFAULT_BOUNCETIME = 50 +DEFAULT_INVERT_LOGIC = False +DEFAULT_PULL_MODE = "UP" + +PIN_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_BOUNCETIME, default=DEFAULT_BOUNCETIME): cv.positive_int, + vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, + vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): vol.In(["UP", "DOWN"]), + } +) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_PINS, default={}): vol.Schema({cv.string: PIN_SCHEMA})} +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Beaglebone Black GPIO devices.""" + pins = config[CONF_PINS] + + binary_sensors = [] + + for pin, params in pins.items(): + binary_sensors.append(BBBGPIOBinarySensor(pin, params)) + add_entities(binary_sensors) + + +class BBBGPIOBinarySensor(BinarySensorEntity): + """Representation of a binary sensor that uses Beaglebone Black GPIO.""" + + def __init__(self, pin, params): + """Initialize the Beaglebone Black binary sensor.""" + self._pin = pin + self._name = params[CONF_NAME] or DEVICE_DEFAULT_NAME + self._bouncetime = params[CONF_BOUNCETIME] + self._pull_mode = params[CONF_PULL_MODE] + self._invert_logic = params[CONF_INVERT_LOGIC] + + bbb_gpio.setup_input(self._pin, self._pull_mode) + self._state = bbb_gpio.read_input(self._pin) + + def read_gpio(pin): + """Read state from GPIO.""" + self._state = bbb_gpio.read_input(self._pin) + self.schedule_update_ha_state() + + bbb_gpio.edge_detect(self._pin, read_gpio, self._bouncetime) + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return the state of the entity.""" + return self._state != self._invert_logic diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/bbb_gpio/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/bbb_gpio/manifest.json new file mode 100644 index 00000000000..add067ab0cc --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/bbb_gpio/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "bbb_gpio", + "name": "BeagleBone Black GPIO", + "documentation": "https://www.home-assistant.io/integrations/bbb_gpio", + "requirements": ["Adafruit_BBIO==1.1.1"], + "codeowners": [], + "iot_class": "local_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/bbb_gpio/switch.py b/homeassistant-2021.6.0.dev0/homeassistant/components/bbb_gpio/switch.py new file mode 100644 index 00000000000..03a9065a15b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/bbb_gpio/switch.py @@ -0,0 +1,79 @@ +"""Allows to configure a switch using BeagleBone Black GPIO.""" +import voluptuous as vol + +from homeassistant.components import bbb_gpio +from homeassistant.components.switch import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity + +CONF_PINS = "pins" +CONF_INITIAL = "initial" +CONF_INVERT_LOGIC = "invert_logic" + +PIN_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_INITIAL, default=False): cv.boolean, + vol.Optional(CONF_INVERT_LOGIC, default=False): cv.boolean, + } +) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_PINS, default={}): vol.Schema({cv.string: PIN_SCHEMA})} +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the BeagleBone Black GPIO devices.""" + pins = config[CONF_PINS] + + switches = [] + for pin, params in pins.items(): + switches.append(BBBGPIOSwitch(pin, params)) + add_entities(switches) + + +class BBBGPIOSwitch(ToggleEntity): + """Representation of a BeagleBone Black GPIO.""" + + def __init__(self, pin, params): + """Initialize the pin.""" + self._pin = pin + self._name = params[CONF_NAME] or DEVICE_DEFAULT_NAME + self._state = params[CONF_INITIAL] + self._invert_logic = params[CONF_INVERT_LOGIC] + + bbb_gpio.setup_output(self._pin) + + if self._state is False: + bbb_gpio.write_output(self._pin, 1 if self._invert_logic else 0) + else: + bbb_gpio.write_output(self._pin, 0 if self._invert_logic else 1) + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the device on.""" + bbb_gpio.write_output(self._pin, 0 if self._invert_logic else 1) + self._state = True + self.schedule_update_ha_state() + + def turn_off(self, **kwargs): + """Turn the device off.""" + bbb_gpio.write_output(self._pin, 1 if self._invert_logic else 0) + self._state = False + self.schedule_update_ha_state() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/bbox/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/bbox/__init__.py new file mode 100644 index 00000000000..8c3bbf0d57f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/bbox/__init__.py @@ -0,0 +1 @@ +"""The bbox component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/bbox/device_tracker.py b/homeassistant-2021.6.0.dev0/homeassistant/components/bbox/device_tracker.py new file mode 100644 index 00000000000..9dac635dd2f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/bbox/device_tracker.py @@ -0,0 +1,97 @@ +"""Support for French FAI Bouygues Bbox routers.""" +from __future__ import annotations + +from collections import namedtuple +from datetime import timedelta +import logging + +import pybbox +import voluptuous as vol + +from homeassistant.components.device_tracker import ( + DOMAIN, + PLATFORM_SCHEMA, + DeviceScanner, +) +from homeassistant.const import CONF_HOST +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle +import homeassistant.util.dt as dt_util + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_HOST = "192.168.1.254" + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string} +) + + +def get_scanner(hass, config): + """Validate the configuration and return a Bbox scanner.""" + scanner = BboxDeviceScanner(config[DOMAIN]) + + return scanner if scanner.success_init else None + + +Device = namedtuple("Device", ["mac", "name", "ip", "last_update"]) + + +class BboxDeviceScanner(DeviceScanner): + """This class scans for devices connected to the bbox.""" + + def __init__(self, config): + """Get host from config.""" + + self.host = config[CONF_HOST] + + """Initialize the scanner.""" + self.last_results: list[Device] = [] + + self.success_init = self._update_info() + _LOGGER.info("Scanner initialized") + + def scan_devices(self): + """Scan for new devices and return a list with found device IDs.""" + self._update_info() + + 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 + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """Check the Bbox for devices. + + Returns boolean if scanning successful. + """ + _LOGGER.info("Scanning") + + box = pybbox.Bbox(ip=self.host) + result = box.get_all_connected_devices() + + now = dt_util.now() + last_results = [] + for device in result: + if device["active"] != 1: + continue + last_results.append( + Device( + device["macaddress"], device["hostname"], device["ipaddress"], now + ) + ) + + self.last_results = last_results + + _LOGGER.info("Scan successful") + return True diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/bbox/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/bbox/manifest.json new file mode 100644 index 00000000000..a59023bb3f5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/bbox/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "bbox", + "name": "Bbox", + "documentation": "https://www.home-assistant.io/integrations/bbox", + "requirements": ["pybbox==0.0.5-alpha"], + "codeowners": [], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/bbox/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/bbox/sensor.py new file mode 100644 index 00000000000..5256c2a61a0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/bbox/sensor.py @@ -0,0 +1,208 @@ +"""Support for Bbox Bouygues Modem Router.""" +from datetime import timedelta +import logging + +import pybbox +import requests +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONF_MONITORED_VARIABLES, + CONF_NAME, + DATA_RATE_MEGABITS_PER_SECOND, + DEVICE_CLASS_TIMESTAMP, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle +from homeassistant.util.dt import utcnow + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = "Powered by Bouygues Telecom" + +DEFAULT_NAME = "Bbox" + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + +# Sensor types are defined like so: Name, unit, icon +SENSOR_TYPES = { + "down_max_bandwidth": [ + "Maximum Download Bandwidth", + DATA_RATE_MEGABITS_PER_SECOND, + "mdi:download", + ], + "up_max_bandwidth": [ + "Maximum Upload Bandwidth", + DATA_RATE_MEGABITS_PER_SECOND, + "mdi:upload", + ], + "current_down_bandwidth": [ + "Currently Used Download Bandwidth", + DATA_RATE_MEGABITS_PER_SECOND, + "mdi:download", + ], + "current_up_bandwidth": [ + "Currently Used Upload Bandwidth", + DATA_RATE_MEGABITS_PER_SECOND, + "mdi:upload", + ], + "uptime": ["Uptime", None, "mdi:clock"], + "number_of_reboots": ["Number of reboot", None, "mdi:restart"], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_MONITORED_VARIABLES): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Bbox sensor.""" + # Create a data fetcher to support all of the configured sensors. Then make + # the first call to init the data. + try: + bbox_data = BboxData() + bbox_data.update() + except requests.exceptions.HTTPError as error: + _LOGGER.error(error) + return False + + name = config[CONF_NAME] + + sensors = [] + for variable in config[CONF_MONITORED_VARIABLES]: + if variable == "uptime": + sensors.append(BboxUptimeSensor(bbox_data, variable, name)) + else: + sensors.append(BboxSensor(bbox_data, variable, name)) + + add_entities(sensors, True) + + +class BboxUptimeSensor(SensorEntity): + """Bbox uptime sensor.""" + + def __init__(self, bbox_data, sensor_type, name): + """Initialize the sensor.""" + self.client_name = name + self.type = sensor_type + self._name = SENSOR_TYPES[sensor_type][0] + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._icon = SENSOR_TYPES[sensor_type][2] + self.bbox_data = bbox_data + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self.client_name} {self._name}" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def device_class(self): + """Return the class of this sensor.""" + return DEVICE_CLASS_TIMESTAMP + + def update(self): + """Get the latest data from Bbox and update the state.""" + self.bbox_data.update() + uptime = utcnow() - timedelta( + seconds=self.bbox_data.router_infos["device"]["uptime"] + ) + self._state = uptime.replace(microsecond=0).isoformat() + + +class BboxSensor(SensorEntity): + """Implementation of a Bbox sensor.""" + + def __init__(self, bbox_data, sensor_type, name): + """Initialize the sensor.""" + self.client_name = name + self.type = sensor_type + self._name = SENSOR_TYPES[sensor_type][0] + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._icon = SENSOR_TYPES[sensor_type][2] + self.bbox_data = bbox_data + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self.client_name} {self._name}" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + def update(self): + """Get the latest data from Bbox and update the state.""" + self.bbox_data.update() + if self.type == "down_max_bandwidth": + self._state = round(self.bbox_data.data["rx"]["maxBandwidth"] / 1000, 2) + elif self.type == "up_max_bandwidth": + self._state = round(self.bbox_data.data["tx"]["maxBandwidth"] / 1000, 2) + elif self.type == "current_down_bandwidth": + self._state = round(self.bbox_data.data["rx"]["bandwidth"] / 1000, 2) + elif self.type == "current_up_bandwidth": + self._state = round(self.bbox_data.data["tx"]["bandwidth"] / 1000, 2) + elif self.type == "number_of_reboots": + self._state = self.bbox_data.router_infos["device"]["numberofboots"] + + +class BboxData: + """Get data from the Bbox.""" + + def __init__(self): + """Initialize the data object.""" + self.data = None + self.router_infos = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from the Bbox.""" + + try: + box = pybbox.Bbox() + self.data = box.get_ip_stats() + self.router_infos = box.get_bbox_info() + except requests.exceptions.HTTPError as error: + _LOGGER.error(error) + self.data = None + self.router_infos = None + return False diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/beewi_smartclim/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/beewi_smartclim/__init__.py new file mode 100644 index 00000000000..f907ce95ae6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/beewi_smartclim/__init__.py @@ -0,0 +1 @@ +"""The beewi_smartclim component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/beewi_smartclim/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/beewi_smartclim/manifest.json new file mode 100644 index 00000000000..941faf1b598 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/beewi_smartclim/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "beewi_smartclim", + "name": "BeeWi SmartClim BLE sensor", + "documentation": "https://www.home-assistant.io/integrations/beewi_smartclim", + "requirements": ["beewi_smartclim==0.0.10"], + "codeowners": ["@alemuro"], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/beewi_smartclim/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/beewi_smartclim/sensor.py new file mode 100644 index 00000000000..9bf935f3c4f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/beewi_smartclim/sensor.py @@ -0,0 +1,104 @@ +"""Platform for beewi_smartclim integration.""" +from beewi_smartclim import BeewiSmartClimPoller # pylint: disable=import-error +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.const import ( + CONF_MAC, + CONF_NAME, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, + TEMP_CELSIUS, +) +import homeassistant.helpers.config_validation as cv + +# Default values +DEFAULT_NAME = "BeeWi SmartClim" + +# Sensor config +SENSOR_TYPES = [ + [DEVICE_CLASS_TEMPERATURE, "Temperature", TEMP_CELSIUS], + [DEVICE_CLASS_HUMIDITY, "Humidity", PERCENTAGE], + [DEVICE_CLASS_BATTERY, "Battery", PERCENTAGE], +] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_MAC): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the beewi_smartclim platform.""" + + mac = config[CONF_MAC] + prefix = config[CONF_NAME] + poller = BeewiSmartClimPoller(mac) + + sensors = [] + + for sensor_type in SENSOR_TYPES: + device = sensor_type[0] + name = sensor_type[1] + unit = sensor_type[2] + # `prefix` is the name configured by the user for the sensor, we're appending + # the device type at the end of the name (garden -> garden temperature) + if prefix: + name = f"{prefix} {name}" + + sensors.append(BeewiSmartclimSensor(poller, name, mac, device, unit)) + + add_entities(sensors) + + +class BeewiSmartclimSensor(SensorEntity): + """Representation of a Sensor.""" + + def __init__(self, poller, name, mac, device, unit): + """Initialize the sensor.""" + self._poller = poller + self._name = name + self._mac = mac + self._device = device + self._unit = unit + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor. State is returned in Celsius.""" + return self._state + + @property + def device_class(self): + """Device class of this entity.""" + return self._device + + @property + def unique_id(self): + """Return a unique, Home Assistant friendly identifier for this entity.""" + return f"{self._mac}_{self._device}" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit + + def update(self): + """Fetch new state data from the poller.""" + self._poller.update_sensor() + self._state = None + if self._device == DEVICE_CLASS_TEMPERATURE: + self._state = self._poller.get_temperature() + if self._device == DEVICE_CLASS_HUMIDITY: + self._state = self._poller.get_humidity() + if self._device == DEVICE_CLASS_BATTERY: + self._state = self._poller.get_battery() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/bh1750/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/bh1750/__init__.py new file mode 100644 index 00000000000..ce7ecc65366 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/bh1750/__init__.py @@ -0,0 +1 @@ +"""The bh1750 component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/bh1750/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/bh1750/manifest.json new file mode 100644 index 00000000000..f784b029a01 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/bh1750/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "bh1750", + "name": "BH1750", + "documentation": "https://www.home-assistant.io/integrations/bh1750", + "requirements": ["i2csense==0.0.4", "smbus-cffi==0.5.1"], + "codeowners": [], + "iot_class": "local_push" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/bh1750/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/bh1750/sensor.py new file mode 100644 index 00000000000..5b708ae2630 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/bh1750/sensor.py @@ -0,0 +1,138 @@ +"""Support for BH1750 light sensor.""" +from functools import partial +import logging + +from i2csense.bh1750 import BH1750 # pylint: disable=import-error +import smbus +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.const import CONF_NAME, DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_I2C_ADDRESS = "i2c_address" +CONF_I2C_BUS = "i2c_bus" +CONF_OPERATION_MODE = "operation_mode" +CONF_SENSITIVITY = "sensitivity" +CONF_DELAY = "measurement_delay_ms" +CONF_MULTIPLIER = "multiplier" + +# Operation modes for BH1750 sensor (from the datasheet). Time typically 120ms +# In one time measurements, device is set to Power Down after each sample. +CONTINUOUS_LOW_RES_MODE = "continuous_low_res_mode" +CONTINUOUS_HIGH_RES_MODE_1 = "continuous_high_res_mode_1" +CONTINUOUS_HIGH_RES_MODE_2 = "continuous_high_res_mode_2" +ONE_TIME_LOW_RES_MODE = "one_time_low_res_mode" +ONE_TIME_HIGH_RES_MODE_1 = "one_time_high_res_mode_1" +ONE_TIME_HIGH_RES_MODE_2 = "one_time_high_res_mode_2" +OPERATION_MODES = { + CONTINUOUS_LOW_RES_MODE: (0x13, True), # 4lx resolution + CONTINUOUS_HIGH_RES_MODE_1: (0x10, True), # 1lx resolution. + CONTINUOUS_HIGH_RES_MODE_2: (0x11, True), # 0.5lx resolution. + ONE_TIME_LOW_RES_MODE: (0x23, False), # 4lx resolution. + ONE_TIME_HIGH_RES_MODE_1: (0x20, False), # 1lx resolution. + ONE_TIME_HIGH_RES_MODE_2: (0x21, False), # 0.5lx resolution. +} + +DEFAULT_NAME = "BH1750 Light Sensor" +DEFAULT_I2C_ADDRESS = "0x23" +DEFAULT_I2C_BUS = 1 +DEFAULT_MODE = CONTINUOUS_HIGH_RES_MODE_1 +DEFAULT_DELAY_MS = 120 +DEFAULT_SENSITIVITY = 69 # from 31 to 254 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): cv.string, + vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): vol.Coerce(int), + vol.Optional(CONF_OPERATION_MODE, default=DEFAULT_MODE): vol.In( + OPERATION_MODES + ), + vol.Optional(CONF_SENSITIVITY, default=DEFAULT_SENSITIVITY): cv.positive_int, + vol.Optional(CONF_DELAY, default=DEFAULT_DELAY_MS): cv.positive_int, + vol.Optional(CONF_MULTIPLIER, default=1.0): vol.Range(min=0.1, max=10), + } +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the BH1750 sensor.""" + + name = config[CONF_NAME] + bus_number = config[CONF_I2C_BUS] + i2c_address = config[CONF_I2C_ADDRESS] + operation_mode = config[CONF_OPERATION_MODE] + + bus = smbus.SMBus(bus_number) + + sensor = await hass.async_add_executor_job( + partial( + BH1750, + bus, + i2c_address, + operation_mode=operation_mode, + measurement_delay=config[CONF_DELAY], + sensitivity=config[CONF_SENSITIVITY], + logger=_LOGGER, + ) + ) + if not sensor.sample_ok: + _LOGGER.error("BH1750 sensor not detected at %s", i2c_address) + return False + + dev = [BH1750Sensor(sensor, name, LIGHT_LUX, config[CONF_MULTIPLIER])] + _LOGGER.info( + "Setup of BH1750 light sensor at %s in mode %s is complete", + i2c_address, + operation_mode, + ) + + async_add_entities(dev, True) + + +class BH1750Sensor(SensorEntity): + """Implementation of the BH1750 sensor.""" + + def __init__(self, bh1750_sensor, name, unit, multiplier=1.0): + """Initialize the sensor.""" + self._name = name + self._unit_of_measurement = unit + self._multiplier = multiplier + self.bh1750_sensor = bh1750_sensor + if self.bh1750_sensor.light_level >= 0: + self._state = int(round(self.bh1750_sensor.light_level)) + else: + self._state = None + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return self._name + + @property + def state(self) -> int: + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self) -> str: + """Return the unit of measurement of the sensor.""" + return self._unit_of_measurement + + @property + def device_class(self) -> str: + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_ILLUMINANCE + + async def async_update(self): + """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._state = int(round(self.bh1750_sensor.light_level * self._multiplier)) + else: + _LOGGER.warning( + "Bad Update of sensor.%s: %s", self.name, self.bh1750_sensor.light_level + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/__init__.py new file mode 100644 index 00000000000..f022509f9de --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/__init__.py @@ -0,0 +1,175 @@ +"""Component to interface with binary sensors.""" + +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.helpers.config_validation import ( # noqa: F401 + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent + +# mypy: allow-untyped-defs, no-check-untyped-defs + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "binary_sensor" +SCAN_INTERVAL = timedelta(seconds=30) + +ENTITY_ID_FORMAT = DOMAIN + ".{}" + +# On means low, Off means normal +DEVICE_CLASS_BATTERY = "battery" + +# On means charging, Off means not charging +DEVICE_CLASS_BATTERY_CHARGING = "battery_charging" + +# On means cold, Off means normal +DEVICE_CLASS_COLD = "cold" + +# On means connected, Off means disconnected +DEVICE_CLASS_CONNECTIVITY = "connectivity" + +# On means open, Off means closed +DEVICE_CLASS_DOOR = "door" + +# On means open, Off means closed +DEVICE_CLASS_GARAGE_DOOR = "garage_door" + +# On means gas detected, Off means no gas (clear) +DEVICE_CLASS_GAS = "gas" + +# On means hot, Off means normal +DEVICE_CLASS_HEAT = "heat" + +# On means light detected, Off means no light +DEVICE_CLASS_LIGHT = "light" + +# On means open (unlocked), Off means closed (locked) +DEVICE_CLASS_LOCK = "lock" + +# On means wet, Off means dry +DEVICE_CLASS_MOISTURE = "moisture" + +# On means motion detected, Off means no motion (clear) +DEVICE_CLASS_MOTION = "motion" + +# On means moving, Off means not moving (stopped) +DEVICE_CLASS_MOVING = "moving" + +# On means occupied, Off means not occupied (clear) +DEVICE_CLASS_OCCUPANCY = "occupancy" + +# On means open, Off means closed +DEVICE_CLASS_OPENING = "opening" + +# On means plugged in, Off means unplugged +DEVICE_CLASS_PLUG = "plug" + +# On means power detected, Off means no power +DEVICE_CLASS_POWER = "power" + +# On means home, Off means away +DEVICE_CLASS_PRESENCE = "presence" + +# On means problem detected, Off means no problem (OK) +DEVICE_CLASS_PROBLEM = "problem" + +# On means unsafe, Off means safe +DEVICE_CLASS_SAFETY = "safety" + +# On means smoke detected, Off means no smoke (clear) +DEVICE_CLASS_SMOKE = "smoke" + +# On means sound detected, Off means no sound (clear) +DEVICE_CLASS_SOUND = "sound" + +# On means vibration detected, Off means no vibration +DEVICE_CLASS_VIBRATION = "vibration" + +# On means open, Off means closed +DEVICE_CLASS_WINDOW = "window" + +DEVICE_CLASSES = [ + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_BATTERY_CHARGING, + DEVICE_CLASS_COLD, + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_HEAT, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_LOCK, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_MOVING, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PLUG, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESENCE, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_SOUND, + DEVICE_CLASS_VIBRATION, + DEVICE_CLASS_WINDOW, +] + +DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) + + +async def async_setup(hass, config): + """Track states and offer events for binary sensors.""" + component = hass.data[DOMAIN] = EntityComponent( + logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL + ) + + await component.async_setup(config) + return True + + +async def async_setup_entry(hass, entry): + """Set up a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.data[DOMAIN].async_unload_entry(entry) + + +class BinarySensorEntity(Entity): + """Represent a binary sensor.""" + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return None + + @property + def state(self): + """Return the state of the binary sensor.""" + return STATE_ON if self.is_on else STATE_OFF + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return None + + +class BinarySensorDevice(BinarySensorEntity): + """Represent a binary sensor (for backwards compatibility).""" + + def __init_subclass__(cls, **kwargs): + """Print deprecation warning.""" + super().__init_subclass__(**kwargs) + _LOGGER.warning( + "BinarySensorDevice is deprecated, modify %s to extend BinarySensorEntity", + cls.__name__, + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/device_condition.py b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/device_condition.py new file mode 100644 index 00000000000..8c506634200 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/device_condition.py @@ -0,0 +1,271 @@ +"""Implement device conditions for binary sensor.""" +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.components.device_automation.const import CONF_IS_OFF, CONF_IS_ON +from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import condition, config_validation as cv +from homeassistant.helpers.entity_registry import ( + async_entries_for_device, + async_get_registry, +) +from homeassistant.helpers.typing import ConfigType + +from . import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_BATTERY_CHARGING, + DEVICE_CLASS_COLD, + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_HEAT, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_LOCK, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_MOVING, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PLUG, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESENCE, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_SOUND, + DEVICE_CLASS_VIBRATION, + DEVICE_CLASS_WINDOW, + DOMAIN, +) + +DEVICE_CLASS_NONE = "none" + +CONF_IS_BAT_LOW = "is_bat_low" +CONF_IS_NOT_BAT_LOW = "is_not_bat_low" +CONF_IS_CHARGING = "is_charging" +CONF_IS_NOT_CHARGING = "is_not_charging" +CONF_IS_COLD = "is_cold" +CONF_IS_NOT_COLD = "is_not_cold" +CONF_IS_CONNECTED = "is_connected" +CONF_IS_NOT_CONNECTED = "is_not_connected" +CONF_IS_GAS = "is_gas" +CONF_IS_NO_GAS = "is_no_gas" +CONF_IS_HOT = "is_hot" +CONF_IS_NOT_HOT = "is_not_hot" +CONF_IS_LIGHT = "is_light" +CONF_IS_NO_LIGHT = "is_no_light" +CONF_IS_LOCKED = "is_locked" +CONF_IS_NOT_LOCKED = "is_not_locked" +CONF_IS_MOIST = "is_moist" +CONF_IS_NOT_MOIST = "is_not_moist" +CONF_IS_MOTION = "is_motion" +CONF_IS_NO_MOTION = "is_no_motion" +CONF_IS_MOVING = "is_moving" +CONF_IS_NOT_MOVING = "is_not_moving" +CONF_IS_OCCUPIED = "is_occupied" +CONF_IS_NOT_OCCUPIED = "is_not_occupied" +CONF_IS_PLUGGED_IN = "is_plugged_in" +CONF_IS_NOT_PLUGGED_IN = "is_not_plugged_in" +CONF_IS_POWERED = "is_powered" +CONF_IS_NOT_POWERED = "is_not_powered" +CONF_IS_PRESENT = "is_present" +CONF_IS_NOT_PRESENT = "is_not_present" +CONF_IS_PROBLEM = "is_problem" +CONF_IS_NO_PROBLEM = "is_no_problem" +CONF_IS_UNSAFE = "is_unsafe" +CONF_IS_NOT_UNSAFE = "is_not_unsafe" +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_VIBRATION = "is_vibration" +CONF_IS_NO_VIBRATION = "is_no_vibration" +CONF_IS_OPEN = "is_open" +CONF_IS_NOT_OPEN = "is_not_open" + +IS_ON = [ + CONF_IS_BAT_LOW, + CONF_IS_CHARGING, + CONF_IS_COLD, + CONF_IS_CONNECTED, + CONF_IS_GAS, + CONF_IS_HOT, + CONF_IS_LIGHT, + CONF_IS_NOT_LOCKED, + CONF_IS_MOIST, + CONF_IS_MOTION, + CONF_IS_MOVING, + CONF_IS_OCCUPIED, + CONF_IS_OPEN, + CONF_IS_PLUGGED_IN, + CONF_IS_POWERED, + CONF_IS_PRESENT, + CONF_IS_PROBLEM, + CONF_IS_SMOKE, + CONF_IS_SOUND, + CONF_IS_UNSAFE, + CONF_IS_VIBRATION, + CONF_IS_ON, +] + +IS_OFF = [ + CONF_IS_NOT_BAT_LOW, + CONF_IS_NOT_CHARGING, + CONF_IS_NOT_COLD, + CONF_IS_NOT_CONNECTED, + CONF_IS_NOT_HOT, + CONF_IS_LOCKED, + CONF_IS_NOT_MOIST, + CONF_IS_NOT_MOVING, + CONF_IS_NOT_OCCUPIED, + CONF_IS_NOT_OPEN, + CONF_IS_NOT_PLUGGED_IN, + CONF_IS_NOT_POWERED, + CONF_IS_NOT_PRESENT, + CONF_IS_NOT_UNSAFE, + CONF_IS_NO_GAS, + CONF_IS_NO_LIGHT, + CONF_IS_NO_MOTION, + CONF_IS_NO_PROBLEM, + CONF_IS_NO_SMOKE, + CONF_IS_NO_SOUND, + CONF_IS_NO_VIBRATION, + CONF_IS_OFF, +] + +ENTITY_CONDITIONS = { + DEVICE_CLASS_BATTERY: [ + {CONF_TYPE: CONF_IS_BAT_LOW}, + {CONF_TYPE: CONF_IS_NOT_BAT_LOW}, + ], + DEVICE_CLASS_BATTERY_CHARGING: [ + {CONF_TYPE: CONF_IS_CHARGING}, + {CONF_TYPE: CONF_IS_NOT_CHARGING}, + ], + DEVICE_CLASS_COLD: [{CONF_TYPE: CONF_IS_COLD}, {CONF_TYPE: CONF_IS_NOT_COLD}], + DEVICE_CLASS_CONNECTIVITY: [ + {CONF_TYPE: CONF_IS_CONNECTED}, + {CONF_TYPE: CONF_IS_NOT_CONNECTED}, + ], + DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], + DEVICE_CLASS_GARAGE_DOOR: [ + {CONF_TYPE: CONF_IS_OPEN}, + {CONF_TYPE: CONF_IS_NOT_OPEN}, + ], + DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_IS_GAS}, {CONF_TYPE: CONF_IS_NO_GAS}], + DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_IS_HOT}, {CONF_TYPE: CONF_IS_NOT_HOT}], + DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_IS_LIGHT}, {CONF_TYPE: CONF_IS_NO_LIGHT}], + DEVICE_CLASS_LOCK: [{CONF_TYPE: CONF_IS_LOCKED}, {CONF_TYPE: CONF_IS_NOT_LOCKED}], + DEVICE_CLASS_MOISTURE: [{CONF_TYPE: CONF_IS_MOIST}, {CONF_TYPE: CONF_IS_NOT_MOIST}], + DEVICE_CLASS_MOTION: [{CONF_TYPE: CONF_IS_MOTION}, {CONF_TYPE: CONF_IS_NO_MOTION}], + DEVICE_CLASS_MOVING: [{CONF_TYPE: CONF_IS_MOVING}, {CONF_TYPE: CONF_IS_NOT_MOVING}], + DEVICE_CLASS_OCCUPANCY: [ + {CONF_TYPE: CONF_IS_OCCUPIED}, + {CONF_TYPE: CONF_IS_NOT_OCCUPIED}, + ], + DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], + DEVICE_CLASS_PLUG: [ + {CONF_TYPE: CONF_IS_PLUGGED_IN}, + {CONF_TYPE: CONF_IS_NOT_PLUGGED_IN}, + ], + DEVICE_CLASS_POWER: [ + {CONF_TYPE: CONF_IS_POWERED}, + {CONF_TYPE: CONF_IS_NOT_POWERED}, + ], + DEVICE_CLASS_PRESENCE: [ + {CONF_TYPE: CONF_IS_PRESENT}, + {CONF_TYPE: CONF_IS_NOT_PRESENT}, + ], + DEVICE_CLASS_PROBLEM: [ + {CONF_TYPE: CONF_IS_PROBLEM}, + {CONF_TYPE: CONF_IS_NO_PROBLEM}, + ], + 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_VIBRATION: [ + {CONF_TYPE: CONF_IS_VIBRATION}, + {CONF_TYPE: CONF_IS_NO_VIBRATION}, + ], + DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], + DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_ON}, {CONF_TYPE: CONF_IS_OFF}], +} + +CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(IS_OFF + IS_ON), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } +) + + +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> list[dict[str, str]]: + """List device conditions.""" + conditions: list[dict[str, str]] = [] + entity_registry = await async_get_registry(hass) + entries = [ + entry + for entry in async_entries_for_device(entity_registry, device_id) + if entry.domain == DOMAIN + ] + + for entry in entries: + device_class = DEVICE_CLASS_NONE + state = hass.states.get(entry.entity_id) + if state and ATTR_DEVICE_CLASS in state.attributes: + device_class = state.attributes[ATTR_DEVICE_CLASS] + + templates = ENTITY_CONDITIONS.get( + device_class, ENTITY_CONDITIONS[DEVICE_CLASS_NONE] + ) + + conditions.extend( + { + **template, + "condition": "device", + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": DOMAIN, + } + for template in templates + ) + + return conditions + + +@callback +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Evaluate state based on configuration.""" + if config_validation: + config = CONDITION_SCHEMA(config) + condition_type = config[CONF_TYPE] + if condition_type in IS_ON: + stat = "on" + else: + stat = "off" + state_config = { + condition.CONF_CONDITION: "state", + condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + condition.CONF_STATE: stat, + } + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] + + return condition.state_from_config(state_config) + + +async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List condition capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/device_trigger.py new file mode 100644 index 00000000000..b87a761a7a1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/device_trigger.py @@ -0,0 +1,252 @@ +"""Provides device triggers for binary sensors.""" +import voluptuous as vol + +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation.const import ( + CONF_TURNED_OFF, + CONF_TURNED_ON, +) +from homeassistant.components.homeassistant.triggers import state as state_trigger +from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_registry import async_entries_for_device + +from . import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_BATTERY_CHARGING, + DEVICE_CLASS_COLD, + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_HEAT, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_LOCK, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_MOVING, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PLUG, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESENCE, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_SOUND, + DEVICE_CLASS_VIBRATION, + DEVICE_CLASS_WINDOW, + DOMAIN, +) + +# mypy: allow-untyped-defs, no-check-untyped-defs + +DEVICE_CLASS_NONE = "none" + +CONF_BAT_LOW = "bat_low" +CONF_NOT_BAT_LOW = "not_bat_low" +CONF_CHARGING = "charging" +CONF_NOT_CHARGING = "not_charging" +CONF_COLD = "cold" +CONF_NOT_COLD = "not_cold" +CONF_CONNECTED = "connected" +CONF_NOT_CONNECTED = "not_connected" +CONF_GAS = "gas" +CONF_NO_GAS = "no_gas" +CONF_HOT = "hot" +CONF_NOT_HOT = "not_hot" +CONF_LIGHT = "light" +CONF_NO_LIGHT = "no_light" +CONF_LOCKED = "locked" +CONF_NOT_LOCKED = "not_locked" +CONF_MOIST = "moist" +CONF_NOT_MOIST = "not_moist" +CONF_MOTION = "motion" +CONF_NO_MOTION = "no_motion" +CONF_MOVING = "moving" +CONF_NOT_MOVING = "not_moving" +CONF_OCCUPIED = "occupied" +CONF_NOT_OCCUPIED = "not_occupied" +CONF_PLUGGED_IN = "plugged_in" +CONF_NOT_PLUGGED_IN = "not_plugged_in" +CONF_POWERED = "powered" +CONF_NOT_POWERED = "not_powered" +CONF_PRESENT = "present" +CONF_NOT_PRESENT = "not_present" +CONF_PROBLEM = "problem" +CONF_NO_PROBLEM = "no_problem" +CONF_UNSAFE = "unsafe" +CONF_NOT_UNSAFE = "not_unsafe" +CONF_SMOKE = "smoke" +CONF_NO_SMOKE = "no_smoke" +CONF_SOUND = "sound" +CONF_NO_SOUND = "no_sound" +CONF_VIBRATION = "vibration" +CONF_NO_VIBRATION = "no_vibration" +CONF_OPENED = "opened" +CONF_NOT_OPENED = "not_opened" + + +TURNED_ON = [ + CONF_BAT_LOW, + CONF_COLD, + CONF_CONNECTED, + CONF_GAS, + CONF_HOT, + CONF_LIGHT, + CONF_NOT_LOCKED, + CONF_MOIST, + CONF_MOTION, + CONF_MOVING, + CONF_OCCUPIED, + CONF_OPENED, + CONF_PLUGGED_IN, + CONF_POWERED, + CONF_PRESENT, + CONF_PROBLEM, + CONF_SMOKE, + CONF_SOUND, + CONF_UNSAFE, + CONF_VIBRATION, + CONF_TURNED_ON, +] + +TURNED_OFF = [ + CONF_NOT_BAT_LOW, + CONF_NOT_COLD, + CONF_NOT_CONNECTED, + CONF_NOT_HOT, + CONF_LOCKED, + CONF_NOT_MOIST, + CONF_NOT_MOVING, + CONF_NOT_OCCUPIED, + CONF_NOT_OPENED, + CONF_NOT_PLUGGED_IN, + CONF_NOT_POWERED, + CONF_NOT_PRESENT, + CONF_NOT_UNSAFE, + CONF_NO_GAS, + CONF_NO_LIGHT, + CONF_NO_MOTION, + CONF_NO_PROBLEM, + CONF_NO_SMOKE, + CONF_NO_SOUND, + CONF_NO_VIBRATION, + CONF_TURNED_OFF, +] + + +ENTITY_TRIGGERS = { + DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_BAT_LOW}, {CONF_TYPE: CONF_NOT_BAT_LOW}], + DEVICE_CLASS_BATTERY_CHARGING: [ + {CONF_TYPE: CONF_CHARGING}, + {CONF_TYPE: CONF_NOT_CHARGING}, + ], + DEVICE_CLASS_COLD: [{CONF_TYPE: CONF_COLD}, {CONF_TYPE: CONF_NOT_COLD}], + DEVICE_CLASS_CONNECTIVITY: [ + {CONF_TYPE: CONF_CONNECTED}, + {CONF_TYPE: CONF_NOT_CONNECTED}, + ], + DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], + DEVICE_CLASS_GARAGE_DOOR: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], + DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_GAS}, {CONF_TYPE: CONF_NO_GAS}], + DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_HOT}, {CONF_TYPE: CONF_NOT_HOT}], + DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_LIGHT}, {CONF_TYPE: CONF_NO_LIGHT}], + DEVICE_CLASS_LOCK: [{CONF_TYPE: CONF_LOCKED}, {CONF_TYPE: CONF_NOT_LOCKED}], + DEVICE_CLASS_MOISTURE: [{CONF_TYPE: CONF_MOIST}, {CONF_TYPE: CONF_NOT_MOIST}], + DEVICE_CLASS_MOTION: [{CONF_TYPE: CONF_MOTION}, {CONF_TYPE: CONF_NO_MOTION}], + DEVICE_CLASS_MOVING: [{CONF_TYPE: CONF_MOVING}, {CONF_TYPE: CONF_NOT_MOVING}], + DEVICE_CLASS_OCCUPANCY: [ + {CONF_TYPE: CONF_OCCUPIED}, + {CONF_TYPE: CONF_NOT_OCCUPIED}, + ], + DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], + DEVICE_CLASS_PLUG: [{CONF_TYPE: CONF_PLUGGED_IN}, {CONF_TYPE: CONF_NOT_PLUGGED_IN}], + DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWERED}, {CONF_TYPE: CONF_NOT_POWERED}], + DEVICE_CLASS_PRESENCE: [{CONF_TYPE: CONF_PRESENT}, {CONF_TYPE: CONF_NOT_PRESENT}], + DEVICE_CLASS_PROBLEM: [{CONF_TYPE: CONF_PROBLEM}, {CONF_TYPE: CONF_NO_PROBLEM}], + 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_VIBRATION: [ + {CONF_TYPE: CONF_VIBRATION}, + {CONF_TYPE: CONF_NO_VIBRATION}, + ], + DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], + DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_TURNED_ON}, {CONF_TYPE: CONF_TURNED_OFF}], +} + + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TURNED_OFF + TURNED_ON), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } +) + + +async def async_attach_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + trigger_type = config[CONF_TYPE] + if trigger_type in TURNED_ON: + to_state = "on" + else: + to_state = "off" + + state_config = { + state_trigger.CONF_PLATFORM: "state", + state_trigger.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state_trigger.CONF_TO: to_state, + } + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] + + state_config = state_trigger.TRIGGER_SCHEMA(state_config) + return await state_trigger.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) + + +async def async_get_triggers(hass, device_id): + """List device triggers.""" + triggers = [] + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entries = [ + entry + for entry in async_entries_for_device(entity_registry, device_id) + if entry.domain == DOMAIN + ] + + for entry in entries: + device_class = DEVICE_CLASS_NONE + state = hass.states.get(entry.entity_id) + if state: + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + + templates = ENTITY_TRIGGERS.get( + device_class, ENTITY_TRIGGERS[DEVICE_CLASS_NONE] + ) + + triggers.extend( + { + **automation, + "platform": "device", + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": DOMAIN, + } + for automation in templates + ) + + return triggers + + +async def async_get_trigger_capabilities(hass, config): + """List trigger capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/group.py b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/group.py new file mode 100644 index 00000000000..234883ffd5a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/group.py @@ -0,0 +1,14 @@ +"""Describe group states.""" + + +from homeassistant.components.group import GroupIntegrationRegistry +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant, callback + + +@callback +def async_describe_on_off_states( + hass: HomeAssistant, registry: GroupIntegrationRegistry +) -> None: + """Describe group on off states.""" + registry.on_off_states({STATE_ON}, STATE_OFF) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/manifest.json new file mode 100644 index 00000000000..be2feb9d207 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "binary_sensor", + "name": "Binary Sensor", + "documentation": "https://www.home-assistant.io/integrations/binary_sensor", + "codeowners": [], + "quality_scale": "internal" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/significant_change.py b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/significant_change.py new file mode 100644 index 00000000000..8421483ba0c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/significant_change.py @@ -0,0 +1,22 @@ +"""Helper to test significant Binary Sensor state changes.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.core import HomeAssistant, callback + + +@callback +def async_check_significant_change( + hass: HomeAssistant, + old_state: str, + old_attrs: dict, + new_state: str, + new_attrs: dict, + **kwargs: Any, +) -> bool | None: + """Test if state significantly changed.""" + if old_state != new_state: + return True + + return False diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/strings.json new file mode 100644 index 00000000000..7380d1be576 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/strings.json @@ -0,0 +1,191 @@ +{ + "title": "Binary sensor", + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} battery is low", + "is_not_bat_low": "{entity_name} battery is normal", + "is_cold": "{entity_name} is cold", + "is_not_cold": "{entity_name} is not cold", + "is_connected": "{entity_name} is connected", + "is_not_connected": "{entity_name} is disconnected", + "is_gas": "{entity_name} is detecting gas", + "is_no_gas": "{entity_name} is not detecting gas", + "is_hot": "{entity_name} is hot", + "is_not_hot": "{entity_name} is not hot", + "is_light": "{entity_name} is detecting light", + "is_no_light": "{entity_name} is not detecting light", + "is_locked": "{entity_name} is locked", + "is_not_locked": "{entity_name} is unlocked", + "is_moist": "{entity_name} is moist", + "is_not_moist": "{entity_name} is dry", + "is_motion": "{entity_name} is detecting motion", + "is_no_motion": "{entity_name} is not detecting motion", + "is_moving": "{entity_name} is moving", + "is_not_moving": "{entity_name} is not moving", + "is_occupied": "{entity_name} is occupied", + "is_not_occupied": "{entity_name} is not occupied", + "is_plugged_in": "{entity_name} is plugged in", + "is_not_plugged_in": "{entity_name} is unplugged", + "is_powered": "{entity_name} is powered", + "is_not_powered": "{entity_name} is not powered", + "is_present": "{entity_name} is present", + "is_not_present": "{entity_name} is not present", + "is_problem": "{entity_name} is detecting problem", + "is_no_problem": "{entity_name} is not detecting problem", + "is_unsafe": "{entity_name} is unsafe", + "is_not_unsafe": "{entity_name} is safe", + "is_smoke": "{entity_name} is detecting smoke", + "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_vibration": "{entity_name} is detecting vibration", + "is_no_vibration": "{entity_name} is not detecting vibration", + "is_open": "{entity_name} is open", + "is_not_open": "{entity_name} is closed", + "is_on": "{entity_name} is on", + "is_off": "{entity_name} is off" + }, + "trigger_type": { + "bat_low": "{entity_name} battery low", + "not_bat_low": "{entity_name} battery normal", + "cold": "{entity_name} became cold", + "not_cold": "{entity_name} became not cold", + "connected": "{entity_name} connected", + "not_connected": "{entity_name} disconnected", + "gas": "{entity_name} started detecting gas", + "no_gas": "{entity_name} stopped detecting gas", + "hot": "{entity_name} became hot", + "not_hot": "{entity_name} became not hot", + "light": "{entity_name} started detecting light", + "no_light": "{entity_name} stopped detecting light", + "locked": "{entity_name} locked", + "not_locked": "{entity_name} unlocked", + "moist": "{entity_name} became moist", + "not_moist": "{entity_name} became dry", + "motion": "{entity_name} started detecting motion", + "no_motion": "{entity_name} stopped detecting motion", + "moving": "{entity_name} started moving", + "not_moving": "{entity_name} stopped moving", + "occupied": "{entity_name} became occupied", + "not_occupied": "{entity_name} became not occupied", + "plugged_in": "{entity_name} plugged in", + "not_plugged_in": "{entity_name} unplugged", + "powered": "{entity_name} powered", + "not_powered": "{entity_name} not powered", + "present": "{entity_name} present", + "not_present": "{entity_name} not present", + "problem": "{entity_name} started detecting problem", + "no_problem": "{entity_name} stopped detecting problem", + "unsafe": "{entity_name} became unsafe", + "not_unsafe": "{entity_name} became safe", + "smoke": "{entity_name} started detecting smoke", + "no_smoke": "{entity_name} stopped detecting smoke", + "sound": "{entity_name} started detecting sound", + "no_sound": "{entity_name} stopped detecting sound", + "vibration": "{entity_name} started detecting vibration", + "no_vibration": "{entity_name} stopped detecting vibration", + "opened": "{entity_name} opened", + "not_opened": "{entity_name} closed", + "turned_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off" + } + }, + "state": { + "battery": { + "off": "Normal", + "on": "Low" + }, + "battery_charging": { + "off": "Not charging", + "on": "Charging" + }, + "cold": { + "off": "[%key:component::binary_sensor::state::battery::off%]", + "on": "Cold" + }, + "connectivity": { + "off": "[%key:common::state::disconnected%]", + "on": "[%key:common::state::connected%]" + }, + "door": { + "off": "[%key:common::state::closed%]", + "on": "[%key:common::state::open%]" + }, + "garage_door": { + "off": "[%key:common::state::closed%]", + "on": "[%key:common::state::open%]" + }, + "gas": { + "off": "Clear", + "on": "Detected" + }, + "heat": { + "off": "[%key:component::binary_sensor::state::battery::off%]", + "on": "Hot" + }, + "light": { + "off": "No light", + "on": "Light detected" + }, + "lock": { + "off": "[%key:common::state::locked%]", + "on": "[%key:common::state::unlocked%]" + }, + "moisture": { + "off": "Dry", + "on": "Wet" + }, + "motion": { + "off": "[%key:component::binary_sensor::state::gas::off%]", + "on": "[%key:component::binary_sensor::state::gas::on%]" + }, + "moving": { + "off": "Not moving", + "on": "Moving" + }, + "occupancy": { + "off": "[%key:component::binary_sensor::state::gas::off%]", + "on": "[%key:component::binary_sensor::state::gas::on%]" + }, + "opening": { + "off": "[%key:common::state::closed%]", + "on": "[%key:common::state::open%]" + }, + "plug": { + "off": "Unplugged", + "on": "Plugged in" + }, + "presence": { + "off": "[%key:component::device_tracker::state::_::not_home%]", + "on": "[%key:component::device_tracker::state::_::home%]" + }, + "problem": { + "off": "OK", + "on": "Problem" + }, + "safety": { + "off": "Safe", + "on": "Unsafe" + }, + "smoke": { + "off": "[%key:component::binary_sensor::state::gas::off%]", + "on": "[%key:component::binary_sensor::state::gas::on%]" + }, + "sound": { + "off": "[%key:component::binary_sensor::state::gas::off%]", + "on": "[%key:component::binary_sensor::state::gas::on%]" + }, + "vibration": { + "off": "[%key:component::binary_sensor::state::gas::off%]", + "on": "[%key:component::binary_sensor::state::gas::on%]" + }, + "window": { + "off": "[%key:common::state::closed%]", + "on": "[%key:common::state::open%]" + }, + "_": { + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]" + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/af.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/af.json new file mode 100644 index 00000000000..c0988c3aa68 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/af.json @@ -0,0 +1,85 @@ +{ + "state": { + "_": { + "off": "Af", + "on": "Aan" + }, + "battery": { + "off": "Normaal", + "on": "Laag" + }, + "cold": { + "off": "Normaal", + "on": "Koud" + }, + "connectivity": { + "off": "Ontkoppel", + "on": "Gekoppel" + }, + "door": { + "off": "Toe", + "on": "Oop" + }, + "garage_door": { + "off": "Toe", + "on": "Oop" + }, + "gas": { + "off": "Ongemerk", + "on": "Bespeur" + }, + "heat": { + "off": "Normaal", + "on": "Warm" + }, + "lock": { + "off": "Gesluit", + "on": "Oopgesluit" + }, + "moisture": { + "off": "Droog", + "on": "Nat" + }, + "motion": { + "off": "Ongemerk", + "on": "Bespeur" + }, + "occupancy": { + "off": "Ongemerk", + "on": "Bespeur" + }, + "opening": { + "off": "Toe", + "on": "Oop" + }, + "presence": { + "off": "Elders", + "on": "Tuis" + }, + "problem": { + "off": "OK", + "on": "Probleem" + }, + "safety": { + "off": "Veilige", + "on": "Onveilige" + }, + "smoke": { + "off": "Ongemerk", + "on": "Bespeur" + }, + "sound": { + "off": "Ongemerk", + "on": "Bespeur" + }, + "vibration": { + "off": "Ongemerk", + "on": "Bespeur" + }, + "window": { + "off": "Toe", + "on": "Oop" + } + }, + "title": "Bin\u00eare sensor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ar.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ar.json new file mode 100644 index 00000000000..7782421ef1c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ar.json @@ -0,0 +1,85 @@ +{ + "state": { + "_": { + "off": "\u0625\u064a\u0642\u0627\u0641", + "on": "\u062a\u0634\u063a\u064a\u0644" + }, + "battery": { + "off": "\u0637\u0628\u064a\u0639\u064a", + "on": "\u0645\u0646\u062e\u0641\u0636" + }, + "cold": { + "off": "\u0637\u0628\u064a\u0639\u064a", + "on": "\u0628\u0627\u0631\u062f" + }, + "connectivity": { + "off": "\u0645\u0641\u0635\u0648\u0644", + "on": "\u0645\u062a\u0635\u0644" + }, + "door": { + "off": "\u0645\u063a\u0644\u0642", + "on": "\u0645\u0641\u062a\u0648\u062d" + }, + "garage_door": { + "off": "\u0645\u063a\u0644\u0642", + "on": "\u0645\u0641\u062a\u0648\u062d" + }, + "gas": { + "off": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0643\u0634\u0641", + "on": "\u062a\u0645 \u0627\u0644\u0643\u0634\u0641" + }, + "heat": { + "off": "\u0637\u0628\u064a\u0639\u064a", + "on": "\u062d\u0627\u0631" + }, + "lock": { + "off": "\u0645\u0642\u0641\u0644", + "on": "\u063a\u064a\u0631 \u0645\u0642\u0641\u0644" + }, + "moisture": { + "off": "\u062c\u0627\u0641", + "on": "\u0645\u0628\u0644\u0644" + }, + "motion": { + "off": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0643\u0634\u0641", + "on": "\u062a\u0645 \u0627\u0644\u0643\u0634\u0641" + }, + "occupancy": { + "off": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0643\u0634\u0641", + "on": "\u062a\u0645 \u0627\u0644\u0643\u0634\u0641" + }, + "opening": { + "off": "\u0645\u0642\u0641\u0644", + "on": "\u0645\u0641\u062a\u0648\u062d" + }, + "presence": { + "off": "\u062e\u0627\u0631\u062c \u0627\u0644\u0645\u0646\u0632\u0644", + "on": "\u0641\u064a \u0627\u0644\u0645\u0646\u0632\u0644" + }, + "problem": { + "off": "\u0645\u0648\u0627\u0641\u0642", + "on": "\u0639\u0637\u0644" + }, + "safety": { + "off": "\u0623\u0645\u0646", + "on": "\u063a\u064a\u0631 \u0623\u0645\u0646" + }, + "smoke": { + "off": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0643\u0634\u0641", + "on": "\u062a\u0645 \u0627\u0644\u0643\u0634\u0641" + }, + "sound": { + "off": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0643\u0634\u0641", + "on": "\u062a\u0645 \u0627\u0644\u0643\u0634\u0641" + }, + "vibration": { + "off": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0643\u0634\u0641", + "on": "\u062a\u0645 \u0627\u0644\u0643\u0634\u0641" + }, + "window": { + "off": "\u0645\u063a\u0644\u0642", + "on": "\u0645\u0641\u062a\u0648\u062d" + } + }, + "title": "\u062c\u0647\u0627\u0632 \u0627\u0633\u062a\u0634\u0639\u0627\u0631 \u062b\u0646\u0627\u0626\u064a" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/bg.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/bg.json new file mode 100644 index 00000000000..2d969af731e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/bg.json @@ -0,0 +1,175 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430 \u0435 \u0438\u0437\u0442\u043e\u0449\u0435\u043d\u0430", + "is_cold": "{entity_name} \u0435 \u0441\u0442\u0443\u0434\u0435\u043d", + "is_connected": "{entity_name} \u0435 \u0441\u0432\u044a\u0440\u0437\u0430\u043d", + "is_gas": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0433\u0430\u0437", + "is_hot": "{entity_name} \u0435 \u0433\u043e\u0440\u0435\u0449", + "is_light": "{entity_name} \u0437\u0430\u0441\u0438\u0447\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430", + "is_locked": "{entity_name} \u0435 \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d", + "is_moist": "{entity_name} \u0435 \u0432\u043b\u0430\u0436\u0435\u043d", + "is_motion": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "is_moving": "{entity_name} \u0441\u0435 \u0434\u0432\u0438\u0436\u0438", + "is_no_gas": "{entity_name} \u043d\u0435 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0433\u0430\u0437", + "is_no_light": "{entity_name} \u043d\u0435 \u0437\u0430\u0441\u0438\u0447\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430", + "is_no_motion": "{entity_name} \u043d\u0435 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "is_no_problem": "{entity_name} \u043d\u0435 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", + "is_no_smoke": "{entity_name} \u043d\u0435 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0438\u043c", + "is_no_sound": "{entity_name} \u043d\u0435 \u0437\u0430\u0441\u0438\u0447\u0430 \u0437\u0432\u0443\u043a", + "is_no_vibration": "{entity_name} \u043d\u0435 \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438", + "is_not_bat_low": "{entity_name} \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430 \u0435 \u0437\u0430\u0440\u0435\u0434\u0435\u043d\u0430", + "is_not_cold": "{entity_name} \u043d\u0435 \u0435 \u0441\u0442\u0443\u0434\u0435\u043d", + "is_not_connected": "{entity_name} \u0435 \u0440\u0430\u0437\u043a\u0430\u0447\u0435\u043d", + "is_not_hot": "{entity_name} \u043d\u0435 \u0435 \u0433\u043e\u0440\u0435\u0449", + "is_not_locked": "{entity_name} \u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d", + "is_not_moist": "{entity_name} \u0435 \u0441\u0443\u0445", + "is_not_moving": "{entity_name} \u043d\u0435 \u0441\u0435 \u0434\u0432\u0438\u0436\u0438", + "is_not_occupied": "{entity_name} \u043d\u0435 \u0435 \u0437\u0430\u0435\u0442", + "is_not_open": "{entity_name} \u0435 \u0437\u0430\u0442\u0432\u043e\u0440\u0435\u043d", + "is_not_plugged_in": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "is_not_powered": "{entity_name} \u043d\u0435 \u0441\u0435 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430", + "is_not_present": "{entity_name} \u043d\u0435 \u0435 \u043d\u0430\u043b\u0438\u0446\u0435", + "is_not_unsafe": "{entity_name} \u0435 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u0435\u043d", + "is_occupied": "{entity_name} \u0435 \u0437\u0430\u0435\u0442", + "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "is_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d", + "is_open": "{entity_name} \u0435 \u043e\u0442\u0432\u043e\u0440\u0435\u043d", + "is_plugged_in": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d", + "is_powered": "{entity_name} \u0441\u0435 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430", + "is_present": "{entity_name} \u043f\u0440\u0438\u0441\u044a\u0441\u0442\u0432\u0430", + "is_problem": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", + "is_smoke": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0438\u043c", + "is_sound": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0437\u0432\u0443\u043a", + "is_unsafe": "{entity_name} \u043d\u0435 \u0435 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u0435\u043d", + "is_vibration": "{entity_name} \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438" + }, + "trigger_type": { + "bat_low": "{entity_name} \u0438\u0437\u0442\u043e\u0449\u0435\u043d\u0430 \u0431\u0430\u0442\u0435\u0440\u0438\u044f", + "cold": "{entity_name} \u0441\u0435 \u0438\u0437\u0441\u0442\u0443\u0434\u0438", + "connected": "{entity_name} \u0441\u0432\u044a\u0440\u0437\u0430\u043d", + "gas": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0433\u0430\u0437", + "hot": "{entity_name} \u0441\u0435 \u0441\u0442\u043e\u043f\u043b\u0438", + "light": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430", + "locked": "{entity_name} \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d", + "moist": "{entity_name} \u0441\u0442\u0430\u043d\u0430 \u0432\u043b\u0430\u0436\u0435\u043d", + "motion": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435 \u043d\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "moving": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "no_gas": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0433\u0430\u0437", + "no_light": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u0437\u0430\u0441\u0438\u0447\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430", + "no_motion": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "no_problem": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", + "no_smoke": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0438\u043c", + "no_sound": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0437\u0432\u0443\u043a", + "no_vibration": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438", + "not_bat_low": "{entity_name} \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430 \u043d\u0435 \u0435 \u0438\u0437\u0442\u043e\u0449\u0435\u043d\u0430", + "not_cold": "{entity_name} \u0441\u0435 \u0441\u0442\u043e\u043f\u043b\u0438", + "not_connected": "{entity_name} \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "not_hot": "{entity_name} \u043e\u0445\u043b\u0430\u0434\u043d\u044f", + "not_locked": "{entity_name} \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d", + "not_moist": "{entity_name} \u0441\u0442\u0430\u0432\u0430 \u0441\u0443\u0445", + "not_moving": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u0441\u0435 \u0434\u0432\u0438\u0436\u0438", + "not_occupied": "{entity_name} \u0432\u0435\u0447\u0435 \u043d\u0435 \u0435 \u0437\u0430\u0435\u0442", + "not_opened": "{entity_name} \u0437\u0430\u0442\u0432\u043e\u0440\u0435\u043d", + "not_plugged_in": "{entity_name} \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "not_powered": "{entity_name} \u043d\u0435 \u0441\u0435 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430", + "not_present": "{entity_name} \u043d\u0435 \u043f\u0440\u0438\u0441\u044a\u0441\u0442\u0432\u0430", + "not_unsafe": "{entity_name} \u0441\u0442\u0430\u043d\u0430 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u0435\u043d", + "occupied": "{entity_name} \u0441\u0442\u0430\u043d\u0430 \u0437\u0430\u0435\u0442", + "opened": "{entity_name} \u0441\u0435 \u043e\u0442\u0432\u043e\u0440\u0438", + "plugged_in": "{entity_name} \u0441\u0435 \u0432\u043a\u043b\u044e\u0447\u0438", + "powered": "{entity_name} \u0441\u0435 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430", + "present": "{entity_name} \u043f\u0440\u0438\u0441\u044a\u0441\u0442\u0432\u0430", + "problem": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", + "smoke": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0438\u043c", + "sound": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u0437\u0430\u0441\u0438\u0447\u0430 \u0437\u0432\u0443\u043a", + "turned_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "turned_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d", + "unsafe": "{entity_name} \u0441\u0442\u0430\u043d\u0430 \u043e\u043f\u0430\u0441\u0435\u043d", + "vibration": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438" + } + }, + "state": { + "_": { + "off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d" + }, + "battery": { + "off": "\u041d\u043e\u0440\u043c\u0430\u043b\u043d\u0430", + "on": "\u0418\u0437\u0442\u043e\u0449\u0435\u043d\u0430" + }, + "cold": { + "off": "\u041d\u043e\u0440\u043c\u0430\u043b\u043d\u043e", + "on": "\u0421\u0442\u0443\u0434\u0435\u043d\u043e" + }, + "connectivity": { + "off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "on": "\u0421\u0432\u044a\u0440\u0437\u0430\u043d" + }, + "door": { + "off": "\u0417\u0430\u0442\u0432\u043e\u0440\u0435\u043d\u0430", + "on": "\u041e\u0442\u0432\u043e\u0440\u0435\u043d\u0430" + }, + "garage_door": { + "off": "\u0417\u0430\u0442\u0432\u043e\u0440\u0435\u043d\u0430", + "on": "\u041e\u0442\u0432\u043e\u0440\u0435\u043d\u0430" + }, + "gas": { + "off": "\u0427\u0438\u0441\u0442\u043e", + "on": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d" + }, + "heat": { + "off": "\u041d\u043e\u0440\u043c\u0430\u043b\u043d\u043e", + "on": "\u0413\u043e\u0440\u0435\u0449\u043e" + }, + "lock": { + "off": "\u0417\u0430\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + }, + "moisture": { + "off": "\u0421\u0443\u0445", + "on": "\u041c\u043e\u043a\u044a\u0440" + }, + "motion": { + "off": "\u0411\u0435\u0437 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "on": "\u0414\u0432\u0438\u0436\u0435\u043d\u0438\u0435" + }, + "occupancy": { + "off": "\u0427\u0438\u0441\u0442\u043e", + "on": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d" + }, + "opening": { + "off": "\u0417\u0430\u0442\u0432\u043e\u0440\u0435\u043d", + "on": "\u041e\u0442\u0432\u043e\u0440\u0435\u043d" + }, + "presence": { + "off": "\u041e\u0442\u0441\u044a\u0441\u0442\u0432\u0430", + "on": "\u0412\u043a\u044a\u0449\u0438" + }, + "problem": { + "off": "\u041e\u041a", + "on": "\u041f\u0440\u043e\u0431\u043b\u0435\u043c" + }, + "safety": { + "off": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u0435\u043d", + "on": "\u041e\u043f\u0430\u0441\u043d\u043e\u0441\u0442" + }, + "smoke": { + "off": "\u0427\u0438\u0441\u0442\u043e", + "on": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d" + }, + "sound": { + "off": "\u0427\u0438\u0441\u0442\u043e", + "on": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d" + }, + "vibration": { + "off": "\u0427\u0438\u0441\u0442\u043e", + "on": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d\u0430" + }, + "window": { + "off": "\u0417\u0430\u0442\u0432\u043e\u0440\u0435\u043d", + "on": "\u041e\u0442\u0432\u043e\u0440\u0435\u043d" + } + }, + "title": "\u0414\u0432\u043e\u0438\u0447\u0435\u043d \u0441\u0435\u043d\u0437\u043e\u0440" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/bs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/bs.json new file mode 100644 index 00000000000..58975af616b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/bs.json @@ -0,0 +1,61 @@ +{ + "state": { + "_": { + "off": "Isklju\u010den", + "on": "Uklju\u010den" + }, + "battery": { + "off": "Normalno", + "on": "Nisko" + }, + "connectivity": { + "off": "Nepovezan", + "on": "Povezan" + }, + "gas": { + "off": "\u010cist", + "on": "Otkriven" + }, + "moisture": { + "off": "Suho", + "on": "Mokar" + }, + "motion": { + "off": "\u010cist", + "on": "Otkriven" + }, + "occupancy": { + "off": "\u010cist", + "on": "Otkriven" + }, + "opening": { + "off": "Zatvoren", + "on": "Otvoren" + }, + "presence": { + "off": "Odsutan", + "on": "Kod ku\u0107e" + }, + "problem": { + "off": "OK", + "on": "Problem" + }, + "safety": { + "off": "Siguran", + "on": "Nesiguran" + }, + "smoke": { + "off": "\u010cist", + "on": "Otkriven" + }, + "sound": { + "off": "\u010cist", + "on": "Otkriven" + }, + "vibration": { + "off": "\u010cist", + "on": "Otkriven" + } + }, + "title": "Binarni senzor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ca.json new file mode 100644 index 00000000000..9c92a50246a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ca.json @@ -0,0 +1,191 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "Bateria de {entity_name} baixa", + "is_cold": "{entity_name} est\u00e0 fred", + "is_connected": "{entity_name} est\u00e0 connectat", + "is_gas": "{entity_name} est\u00e0 detectant gas", + "is_hot": "{entity_name} est\u00e0 calent", + "is_light": "{entity_name} est\u00e0 detectant llum", + "is_locked": "{entity_name} est\u00e0 bloquejat", + "is_moist": "{entity_name} est\u00e0 humit", + "is_motion": "{entity_name} est\u00e0 detectant moviment", + "is_moving": "{entity_name} s'est\u00e0 movent", + "is_no_gas": "{entity_name} no detecta gas", + "is_no_light": "{entity_name} no detecta llum", + "is_no_motion": "{entity_name} no detecta moviment", + "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_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", + "is_not_connected": "{entity_name} est\u00e0 desconnectat", + "is_not_hot": "{entity_name} no est\u00e0 calent", + "is_not_locked": "{entity_name} est\u00e0 desbloquejat", + "is_not_moist": "{entity_name} est\u00e0 sec", + "is_not_moving": "{entity_name} no s'est\u00e0 movent", + "is_not_occupied": "{entity_name} no est\u00e0 ocupat", + "is_not_open": "{entity_name} est\u00e0 tancat", + "is_not_plugged_in": "{entity_name} est\u00e0 desendollat", + "is_not_powered": "{entity_name} no est\u00e0 alimentat", + "is_not_present": "{entity_name} no est\u00e0 present", + "is_not_unsafe": "{entity_name} \u00e9s segur", + "is_occupied": "{entity_name} est\u00e0 ocupat", + "is_off": "{entity_name} est\u00e0 apagat", + "is_on": "{entity_name} est\u00e0 enc\u00e8s", + "is_open": "{entity_name} est\u00e0 obert", + "is_plugged_in": "{entity_name} est\u00e0 endollat", + "is_powered": "{entity_name} est\u00e0 alimentat", + "is_present": "{entity_name} est\u00e0 present", + "is_problem": "{entity_name} est\u00e0 detectant un problema", + "is_smoke": "{entity_name} est\u00e0 detectant fum", + "is_sound": "{entity_name} est\u00e0 detectant so", + "is_unsafe": "{entity_name} \u00e9s insegur", + "is_vibration": "{entity_name} est\u00e0 detectant vibraci\u00f3" + }, + "trigger_type": { + "bat_low": "Bateria de {entity_name} baixa", + "cold": "{entity_name} es torna fred", + "connected": "{entity_name} est\u00e0 connectat", + "gas": "{entity_name} ha comen\u00e7at a detectar gas", + "hot": "{entity_name} es torna calent", + "light": "{entity_name} ha comen\u00e7at a detectar llum", + "locked": "{entity_name} est\u00e0 bloquejat", + "moist": "{entity_name} es torna humit", + "motion": "{entity_name} ha comen\u00e7at a detectar moviment", + "moving": "{entity_name} ha comen\u00e7at a moure's", + "no_gas": "{entity_name} ha deixat de detectar gas", + "no_light": "{entity_name} ha deixat de detectar llum", + "no_motion": "{entity_name} ha deixat de detectar moviment", + "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_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", + "not_connected": "{entity_name} est\u00e0 desconnectat", + "not_hot": "{entity_name} es torna no-calent", + "not_locked": "{entity_name} est\u00e0 desbloquejat", + "not_moist": "{entity_name} es torna sec", + "not_moving": "{entity_name} ha parat de moure's", + "not_occupied": "{entity_name} es desocupa", + "not_opened": "{entity_name} es tanca", + "not_plugged_in": "{entity_name} desendollat", + "not_powered": "{entity_name} no est\u00e0 alimentat", + "not_present": "{entity_name} no est\u00e0 present", + "not_unsafe": "{entity_name} es torna segur", + "occupied": "{entity_name} s'ocupa", + "opened": "{entity_name} s'ha obert", + "plugged_in": "{entity_name} s'ha endollat", + "powered": "{entity_name} alimentat", + "present": "{entity_name} present", + "problem": "{entity_name} ha comen\u00e7at a detectar un problema", + "smoke": "{entity_name} ha comen\u00e7at a detectar fum", + "sound": "{entity_name} ha comen\u00e7at a detectar so", + "turned_off": "{entity_name} apagat", + "turned_on": "{entity_name} enc\u00e8s", + "unsafe": "{entity_name} es torna insegur", + "vibration": "{entity_name} ha comen\u00e7at a detectar vibraci\u00f3" + } + }, + "state": { + "_": { + "off": "OFF", + "on": "ON" + }, + "battery": { + "off": "Normal", + "on": "Baixa" + }, + "battery_charging": { + "off": "No carregant", + "on": "Carregant" + }, + "cold": { + "off": "Normal", + "on": "Fred" + }, + "connectivity": { + "off": "Desconnectat", + "on": "Connectat" + }, + "door": { + "off": "Tancat/ada", + "on": "Obert/a" + }, + "garage_door": { + "off": "Tancat/ada", + "on": "Obert/a" + }, + "gas": { + "off": "Lliure", + "on": "Detectat" + }, + "heat": { + "off": "Normal", + "on": "Calent" + }, + "light": { + "off": "No s'ha detectat llum", + "on": "Llum detectada" + }, + "lock": { + "off": "Bloquejat", + "on": "Desbloquejat" + }, + "moisture": { + "off": "Sec", + "on": "Humit" + }, + "motion": { + "off": "Lliure", + "on": "Detectat" + }, + "moving": { + "off": "Parat", + "on": "En moviment" + }, + "occupancy": { + "off": "Lliure", + "on": "Detectat" + }, + "opening": { + "off": "Tancat/ada", + "on": "Obert/a" + }, + "plug": { + "off": "Desendollat", + "on": "Endollat" + }, + "presence": { + "off": "Fora", + "on": "A casa" + }, + "problem": { + "off": "OK", + "on": "Problema" + }, + "safety": { + "off": "Segur", + "on": "No segur" + }, + "smoke": { + "off": "Lliure", + "on": "Detectat" + }, + "sound": { + "off": "Lliure", + "on": "Detectat" + }, + "vibration": { + "off": "Lliure", + "on": "Detectat" + }, + "window": { + "off": "Tancat/ada", + "on": "Obert/a" + } + }, + "title": "Sensor binari" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/cs.json new file mode 100644 index 00000000000..90f25332bdb --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/cs.json @@ -0,0 +1,191 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "Baterie {entity_name} je skoro vybit\u00e1", + "is_cold": "{entity_name} je studen\u00fd", + "is_connected": "{entity_name} je p\u0159ipojen", + "is_gas": "{entity_name} detekuje plyn", + "is_hot": "{entity_name} je hork\u00fd", + "is_light": "{entity_name} detekuje sv\u011btlo", + "is_locked": "{entity_name} je zam\u010deno", + "is_moist": "{entity_name} je vlhk\u00fd", + "is_motion": "{entity_name} detekuje pohyb", + "is_moving": "{entity_name} se pohybuje", + "is_no_gas": "{entity_name} nedetekuje plyn", + "is_no_light": "{entity_name} nedetekuje sv\u011btlo", + "is_no_motion": "{entity_name} nedetekuje pohyb", + "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_vibration": "{entity_name} nedetekuje vibrace", + "is_not_bat_low": "{entity_name} baterie v norm\u00e1lu", + "is_not_cold": "{entity_name} nen\u00ed studen\u00fd", + "is_not_connected": "{entity_name} je odpojen", + "is_not_hot": "{entity_name} nen\u00ed hork\u00fd", + "is_not_locked": "{entity_name} je odem\u010den", + "is_not_moist": "{entity_name} je such\u00fd", + "is_not_moving": "{entity_name} se nepohybuje", + "is_not_occupied": "{entity_name} nen\u00ed obsazeno", + "is_not_open": "{entity_name} je zav\u0159eno", + "is_not_plugged_in": "{entity_name} je odpojeno", + "is_not_powered": "{entity_name} nen\u00ed nap\u00e1jeno", + "is_not_present": "{entity_name} nen\u00ed p\u0159\u00edtomno", + "is_not_unsafe": "{entity_name} je bezpe\u010dno", + "is_occupied": "{entity_name} je obsazeno", + "is_off": "{entity_name} je vypnuto", + "is_on": "{entity_name} je zapnuto", + "is_open": "{entity_name} je otev\u0159eno", + "is_plugged_in": "{entity_name} je p\u0159ipojeno", + "is_powered": "{entity_name} nap\u00e1jeno", + "is_present": "{entity_name} p\u0159\u00edtomno", + "is_problem": "{entity_name} detekuje probl\u00e9m", + "is_smoke": "{entity_name} detekuje kou\u0159", + "is_sound": "{entity_name} detekuje zvuk", + "is_unsafe": "{entity_name} nen\u00ed bezpe\u010dno", + "is_vibration": "{entity_name} detekuje vibrace" + }, + "trigger_type": { + "bat_low": "{entity_name} vybit\u00e1 baterie", + "cold": "{entity_name} vychladlo", + "connected": "{entity_name} p\u0159ipojeno", + "gas": "{entity_name} za\u010dalo detekovat plyn", + "hot": "{entity_name} se zah\u0159\u00e1l", + "light": "{entity_name} za\u010dalo detekovat sv\u011btlo", + "locked": "{entity_name} zam\u010deno", + "moist": "{entity_name} zvlhnul", + "motion": "{entity_name} za\u010dalo detekovat pohyb", + "moving": "{entity_name} se za\u010dal pohybovat", + "no_gas": "{entity_name} p\u0159estalo detekovat plyn", + "no_light": "{entity_name} p\u0159estalo detekovat sv\u011btlo", + "no_motion": "{entity_name} p\u0159estalo detekovat pohyb", + "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_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", + "not_connected": "{entity_name} odpojeno", + "not_hot": "{entity_name} p\u0159estal b\u00fdt hork\u00fd", + "not_locked": "{entity_name} odem\u010deno", + "not_moist": "{entity_name} vyschlo", + "not_moving": "{entity_name} se p\u0159estalo pohybovat", + "not_occupied": "{entity_name} volno", + "not_opened": "{entity_name} uzav\u0159eno", + "not_plugged_in": "{entity_name} odpojeno", + "not_powered": "{entity_name} nen\u00ed nap\u00e1jeno", + "not_present": "{entity_name} nep\u0159\u00edtomno", + "not_unsafe": "{entity_name} bezpe\u010dno", + "occupied": "{entity_name} obsazeno", + "opened": "{entity_name} otev\u0159eno", + "plugged_in": "{entity_name} p\u0159ipojeno", + "powered": "{entity_name} nap\u00e1jeno", + "present": "{entity_name} p\u0159\u00edtomno", + "problem": "{entity_name} za\u010dalo detekovat probl\u00e9m", + "smoke": "{entity_name} za\u010dalo detekovat kou\u0159", + "sound": "{entity_name} za\u010dalo detekovat zvuk", + "turned_off": "{entity_name} vypnuto", + "turned_on": "{entity_name} zapnuto", + "unsafe": "{entity_name} hl\u00e1s\u00ed ohro\u017een\u00ed", + "vibration": "{entity_name} za\u010dalo detekovat vibrace" + } + }, + "state": { + "_": { + "off": "Neaktivn\u00ed", + "on": "Aktivn\u00ed" + }, + "battery": { + "off": "Norm\u00e1ln\u00ed", + "on": "N\u00edzk\u00fd stav" + }, + "battery_charging": { + "off": "Nenab\u00edj\u00ed se", + "on": "Nab\u00edjen\u00ed" + }, + "cold": { + "off": "Norm\u00e1ln\u00ed", + "on": "Studen\u00e9" + }, + "connectivity": { + "off": "Odpojeno", + "on": "P\u0159ipojeno" + }, + "door": { + "off": "Zav\u0159eno", + "on": "Otev\u0159eno" + }, + "garage_door": { + "off": "Zav\u0159eno", + "on": "Otev\u0159eno" + }, + "gas": { + "off": "\u017d\u00e1dn\u00fd plyn", + "on": "Zji\u0161t\u011bn plyn" + }, + "heat": { + "off": "Norm\u00e1ln\u00ed", + "on": "Hork\u00e9" + }, + "light": { + "off": "\u017d\u00e1dn\u00e9 sv\u011btlo", + "on": "Zji\u0161t\u011bno sv\u011btlo" + }, + "lock": { + "off": "Zam\u010deno", + "on": "Odem\u010deno" + }, + "moisture": { + "off": "Sucho", + "on": "Vlhko" + }, + "motion": { + "off": "\u017d\u00e1dn\u00fd pohyb", + "on": "Zaznamen\u00e1n pohyb" + }, + "moving": { + "off": "Neh\u00fdbe se", + "on": "V pohybu" + }, + "occupancy": { + "off": "Volno", + "on": "Obsazeno" + }, + "opening": { + "off": "Zav\u0159eno", + "on": "Otev\u0159eno" + }, + "plug": { + "off": "Odpojeno", + "on": "Zapojeno" + }, + "presence": { + "off": "Pry\u010d", + "on": "Doma" + }, + "problem": { + "off": "V po\u0159\u00e1dku", + "on": "Probl\u00e9m" + }, + "safety": { + "off": "Zaji\u0161t\u011bno", + "on": "Nezaji\u0161t\u011bno" + }, + "smoke": { + "off": "\u017d\u00e1dn\u00fd d\u00fdm", + "on": "Zji\u0161t\u011bn d\u00fdm" + }, + "sound": { + "off": "Ticho", + "on": "Zachycen zvuk" + }, + "vibration": { + "off": "Klid", + "on": "Zji\u0161t\u011bny vibrace" + }, + "window": { + "off": "Zav\u0159eno", + "on": "Otev\u0159eno" + } + }, + "title": "Bin\u00e1rn\u00ed senzor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/cy.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/cy.json new file mode 100644 index 00000000000..d28227d7c39 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/cy.json @@ -0,0 +1,85 @@ +{ + "state": { + "_": { + "off": "i ffwrdd", + "on": "Ar" + }, + "battery": { + "off": "Arferol", + "on": "Isel" + }, + "cold": { + "off": "Arferol", + "on": "Oer" + }, + "connectivity": { + "off": "Wedi datgysylltu", + "on": "Cysylltiedig" + }, + "door": { + "off": "Cau", + "on": "Agor" + }, + "garage_door": { + "off": "Cau", + "on": "Agor" + }, + "gas": { + "off": "Clir", + "on": "Wedi'i ganfod" + }, + "heat": { + "off": "Arferol", + "on": "Poeth" + }, + "lock": { + "off": "Cloi", + "on": "Dad-gloi" + }, + "moisture": { + "off": "Sych", + "on": "Gwlyb" + }, + "motion": { + "off": "Clir", + "on": "Wedi'i ganfod" + }, + "occupancy": { + "off": "Clir", + "on": "Wedi'i ganfod" + }, + "opening": { + "off": "Cau", + "on": "Agor" + }, + "presence": { + "off": "Allan", + "on": "Gartref" + }, + "problem": { + "off": "iawn", + "on": "Problem" + }, + "safety": { + "off": "Diogel", + "on": "Anniogel" + }, + "smoke": { + "off": "Clir", + "on": "Wedi'i ganfod" + }, + "sound": { + "off": "Clir", + "on": "Wedi'i ganfod" + }, + "vibration": { + "off": "Clir", + "on": "Wedi'i ganfod" + }, + "window": { + "off": "Cau", + "on": "Agored" + } + }, + "title": "Synhwyrydd deuaidd" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/da.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/da.json new file mode 100644 index 00000000000..7215c5a3556 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/da.json @@ -0,0 +1,175 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} batteri er lavt", + "is_cold": "{entity_name} er kold", + "is_connected": "{entity_name} er tilsluttet", + "is_gas": "{entity_name} registrerer gas", + "is_hot": "{entity_name} er varm", + "is_light": "{entity_name} registrerer lys", + "is_locked": "{entity_name} er l\u00e5st", + "is_moist": "{entity_name} er fugtig", + "is_motion": "{entity_name} registrerer bev\u00e6gelse", + "is_moving": "{entity_name} bev\u00e6ger sig", + "is_no_gas": "{entity_name} registrerer ikke gas", + "is_no_light": "{entity_name} registrerer ikke lys", + "is_no_motion": "{entity_name} registrerer ikke bev\u00e6gelse", + "is_no_problem": "{entity_name} registrerer ikke noget problem", + "is_no_smoke": "{entity_name} registrerer ikke r\u00f8g", + "is_no_sound": "{entity_name} registrerer ikke lyd", + "is_no_vibration": "{entity_name} registrerer ikke vibration", + "is_not_bat_low": "{entity_name} batteri er normalt", + "is_not_cold": "{entity_name} er ikke kold", + "is_not_connected": "{entity_name} er afbrudt", + "is_not_hot": "{entity_name} er ikke varm", + "is_not_locked": "{entity_name} er l\u00e5st op", + "is_not_moist": "{entity_name} er t\u00f8r", + "is_not_moving": "{entity_name} bev\u00e6ger sig ikke", + "is_not_occupied": "{entity_name} er ikke optaget", + "is_not_open": "{entity_name} er lukket", + "is_not_plugged_in": "{entity_name} er ikke tilsluttet str\u00f8m", + "is_not_powered": "{entity_name} er ikke tilsluttet str\u00f8m", + "is_not_present": "{entity_name} er ikke til stede", + "is_not_unsafe": "{entity_name} er sikker", + "is_occupied": "{entity_name} er optaget", + "is_off": "{entity_name} er sl\u00e5et fra", + "is_on": "{entity_name} er sl\u00e5et til", + "is_open": "{entity_name} er \u00e5ben", + "is_plugged_in": "{entity_name} er tilsluttet str\u00f8m", + "is_powered": "{entity_name} er tilsluttet str\u00f8m", + "is_present": "{entity_name} er til stede", + "is_problem": "{entity_name} registrerer problem", + "is_smoke": "{entity_name} registrerer r\u00f8g", + "is_sound": "{entity_name} registrerer lyd", + "is_unsafe": "{entity_name} er usikker", + "is_vibration": "{entity_name} registrerer vibration" + }, + "trigger_type": { + "bat_low": "{entity_name} lavt batteriniveau", + "cold": "{entity_name} blev kold", + "connected": "{entity_name} tilsluttet", + "gas": "{entity_name} begyndte at registrere gas", + "hot": "{entity_name} blev varm", + "light": "{entity_name} begyndte at registrere lys", + "locked": "{entity_name} l\u00e5st", + "moist": "{entity_name} blev fugtig", + "motion": "{entity_name} begyndte at registrere bev\u00e6gelse", + "moving": "{entity_name} begyndte at bev\u00e6ge sig", + "no_gas": "{entity_name} stoppede med at registrere gas", + "no_light": "{entity_name} stoppede med at registrere lys", + "no_motion": "{entity_name} stoppede med at registrere bev\u00e6gelse", + "no_problem": "{entity_name} stoppede med at registrere problem", + "no_smoke": "{entity_name} stoppede med at registrere r\u00f8g", + "no_sound": "{entity_name} stoppede med at registrere lyd", + "no_vibration": "{entity_name} stoppede med at registrere vibration", + "not_bat_low": "{entity_name} batteri normalt", + "not_cold": "{entity_name} blev ikke kold", + "not_connected": "{entity_name} afbrudt", + "not_hot": "{entity_name} blev ikke varm", + "not_locked": "{entity_name} l\u00e5st op", + "not_moist": "{entity_name} blev t\u00f8r", + "not_moving": "{entity_name} stoppede med at bev\u00e6ge sig", + "not_occupied": "{entity_name} blev ikke optaget", + "not_opened": "{entity_name} lukket", + "not_plugged_in": "{entity_name} ikke tilsluttet str\u00f8m", + "not_powered": "{entity_name} ikke tilsluttet str\u00f8m", + "not_present": "{entity_name} ikke til stede", + "not_unsafe": "{entity_name} blev sikker", + "occupied": "{entity_name} blev optaget", + "opened": "{entity_name} \u00e5bnet", + "plugged_in": "{entity_name} tilsluttet str\u00f8m", + "powered": "{entity_name} tilsluttet str\u00f8m", + "present": "{entity_name} til stede", + "problem": "{entity_name} begyndte at registrere problem", + "smoke": "{entity_name} begyndte at registrere r\u00f8g", + "sound": "{entity_name} begyndte at registrere lyd", + "turned_off": "{entity_name} slukkede", + "turned_on": "{entity_name} t\u00e6ndte", + "unsafe": "{entity_name} blev usikker", + "vibration": "{entity_name} begyndte at registrere vibration" + } + }, + "state": { + "_": { + "off": "Fra", + "on": "Til" + }, + "battery": { + "off": "Normal", + "on": "Lav" + }, + "cold": { + "off": "Normal", + "on": "Kold" + }, + "connectivity": { + "off": "Afbrudt", + "on": "Forbundet" + }, + "door": { + "off": "Lukket", + "on": "\u00c5ben" + }, + "garage_door": { + "off": "Lukket", + "on": "\u00c5ben" + }, + "gas": { + "off": "Ikke registreret", + "on": "Registreret" + }, + "heat": { + "off": "Normal", + "on": "Varm" + }, + "lock": { + "off": "L\u00e5st", + "on": "Ul\u00e5st" + }, + "moisture": { + "off": "T\u00f8r", + "on": "Fugtig" + }, + "motion": { + "off": "Ikke registreret", + "on": "Registreret" + }, + "occupancy": { + "off": "Ikke registreret", + "on": "Registreret" + }, + "opening": { + "off": "Lukket", + "on": "\u00c5ben" + }, + "presence": { + "off": "Ude", + "on": "Hjemme" + }, + "problem": { + "off": "OK", + "on": "Problem" + }, + "safety": { + "off": "Sikret", + "on": "Usikret" + }, + "smoke": { + "off": "Ikke registreret", + "on": "Registreret" + }, + "sound": { + "off": "Ikke registreret", + "on": "Registreret" + }, + "vibration": { + "off": "Ikke registreret", + "on": "Registreret" + }, + "window": { + "off": "Lukket", + "on": "\u00c5ben" + } + }, + "title": "Bin\u00e6r sensor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/de.json new file mode 100644 index 00000000000..a78befb7965 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/de.json @@ -0,0 +1,191 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} Batterie ist schwach", + "is_cold": "{entity_name} ist kalt", + "is_connected": "{entity_name} ist verbunden", + "is_gas": "{entity_name} erkennt Gas", + "is_hot": "{entity_name} ist hei\u00df", + "is_light": "{entity_name} erkennt Licht", + "is_locked": "{entity_name} ist gesperrt", + "is_moist": "{entity_name} ist feucht", + "is_motion": "{entity_name} erkennt Bewegung", + "is_moving": "{entity_name} bewegt sich", + "is_no_gas": "{entity_name} erkennt kein Gas", + "is_no_light": "{entity_name} erkennt kein Licht", + "is_no_motion": "{entity_name} erkennt keine Bewegung", + "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_vibration": "{entity_name} erkennt keine Vibrationen", + "is_not_bat_low": "{entity_name} Batterie ist normal", + "is_not_cold": "{entity_name} ist nicht kalt", + "is_not_connected": "{entity_name} ist nicht verbunden", + "is_not_hot": "{entity_name} ist nicht hei\u00df", + "is_not_locked": "{entity_name} ist entsperrt", + "is_not_moist": "{entity_name} ist trocken", + "is_not_moving": "{entity_name} bewegt sich nicht", + "is_not_occupied": "{entity_name} ist nicht besch\u00e4ftigt / besetzt", + "is_not_open": "{entity_name} ist geschlossen", + "is_not_plugged_in": "{entity_name} ist nicht angeschlossen", + "is_not_powered": "{entity_name} wird nicht mit Strom versorgt", + "is_not_present": "{entity_name} ist nicht vorhanden", + "is_not_unsafe": "{entity_name} ist sicher", + "is_occupied": "{entity_name} ist besch\u00e4ftigt / besetzt", + "is_off": "{entity_name} ist ausgeschaltet", + "is_on": "{entity_name} ist eingeschaltet", + "is_open": "{entity_name} ist offen", + "is_plugged_in": "{entity_name} ist eingesteckt", + "is_powered": "{entity_name} wird mit Strom versorgt", + "is_present": "{entity_name} ist vorhanden", + "is_problem": "{entity_name} hat ein Problem festgestellt", + "is_smoke": "{entity_name} hat Rauch detektiert", + "is_sound": "{entity_name} hat Ger\u00e4usche detektiert", + "is_unsafe": "{entity_name} ist unsicher", + "is_vibration": "{entity_name} erkennt Vibrationen." + }, + "trigger_type": { + "bat_low": "{entity_name} Batterie schwach", + "cold": "{entity_name} wurde kalt", + "connected": "{entity_name} verbunden", + "gas": "{entity_name} hat Gas detektiert", + "hot": "{entity_name} wurde hei\u00df", + "light": "{entity_name} hat Licht detektiert", + "locked": "{entity_name} gesperrt", + "moist": "{entity_name} wurde feucht", + "motion": "{entity_name} hat Bewegungen detektiert", + "moving": "{entity_name} hat angefangen sich zu bewegen", + "no_gas": "{entity_name} hat kein Gas mehr erkannt", + "no_light": "{entity_name} hat kein Licht mehr erkannt", + "no_motion": "{entity_name} hat keine Bewegung mehr erkannt", + "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_vibration": "{entity_name}hat keine Vibrationen mehr erkannt", + "not_bat_low": "{entity_name} Batterie normal", + "not_cold": "{entity_name} w\u00e4rmte auf", + "not_connected": "{entity_name} getrennt", + "not_hot": "{entity_name} k\u00fchlte ab", + "not_locked": "{entity_name} entsperrt", + "not_moist": "{entity_name} wurde trocken", + "not_moving": "{entity_name} bewegt sich nicht mehr", + "not_occupied": "{entity_name} wurde frei / inaktiv", + "not_opened": "{entity_name} geschlossen", + "not_plugged_in": "{entity_name} ist nicht angeschlossen", + "not_powered": "{entity_name} nicht mit Strom versorgt", + "not_present": "{entity_name} nicht anwesend", + "not_unsafe": "{entity_name} wurde sicher", + "occupied": "{entity_name} wurde besch\u00e4ftigt / besetzt", + "opened": "{entity_name} ge\u00f6ffnet", + "plugged_in": "{entity_name} eingesteckt", + "powered": "{entity_name} wird mit Strom versorgt", + "present": "{entity_name} anwesend", + "problem": "{entity_name} hat ein Problem festgestellt", + "smoke": "{entity_name} detektiert Rauch", + "sound": "{entity_name} detektiert Ger\u00e4usche", + "turned_off": "{entity_name} ausgeschaltet", + "turned_on": "{entity_name} eingeschaltet", + "unsafe": "{entity_name} ist unsicher", + "vibration": "{entity_name} detektiert Vibrationen" + } + }, + "state": { + "_": { + "off": "Aus", + "on": "An" + }, + "battery": { + "off": "Normal", + "on": "Schwach" + }, + "battery_charging": { + "off": "L\u00e4dt nicht", + "on": "L\u00e4dt" + }, + "cold": { + "off": "Normal", + "on": "Kalt" + }, + "connectivity": { + "off": "Getrennt", + "on": "Verbunden" + }, + "door": { + "off": "Geschlossen", + "on": "Offen" + }, + "garage_door": { + "off": "Geschlossen", + "on": "Offen" + }, + "gas": { + "off": "Normal", + "on": "Erkannt" + }, + "heat": { + "off": "Normal", + "on": "Hei\u00df" + }, + "light": { + "off": "Kein Licht", + "on": "Licht erkannt" + }, + "lock": { + "off": "Verriegelt", + "on": "Entriegelt" + }, + "moisture": { + "off": "Trocken", + "on": "Nass" + }, + "motion": { + "off": "Ruhig", + "on": "Bewegung erkannt" + }, + "moving": { + "off": "Bewegt sich nicht", + "on": "Bewegt sich" + }, + "occupancy": { + "off": "Frei", + "on": "Belegt" + }, + "opening": { + "off": "Geschlossen", + "on": "Offen" + }, + "plug": { + "off": "Ausgesteckt", + "on": "Eingesteckt" + }, + "presence": { + "off": "Abwesend", + "on": "Zu Hause" + }, + "problem": { + "off": "OK", + "on": "Problem" + }, + "safety": { + "off": "Sicher", + "on": "Unsicher" + }, + "smoke": { + "off": "OK", + "on": "Rauch erkannt" + }, + "sound": { + "off": "Stille", + "on": "Ger\u00e4usch erkannt" + }, + "vibration": { + "off": "Normal", + "on": "Vibration" + }, + "window": { + "off": "Geschlossen", + "on": "Offen" + } + }, + "title": "Bin\u00e4rsensor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/el.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/el.json new file mode 100644 index 00000000000..f4ed1d55bc2 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/el.json @@ -0,0 +1,85 @@ +{ + "state": { + "_": { + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc\u03c2", + "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc\u03c2" + }, + "battery": { + "off": "\u039a\u03b1\u03bd\u03bf\u03bd\u03b9\u03ba\u03cc\u03c2", + "on": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03cc\u03c2" + }, + "cold": { + "off": "\u03a6\u03c5\u03c3\u03b9\u03bf\u03bb\u03bf\u03b3\u03b9\u03ba\u03cc", + "on": "\u039a\u03c1\u03cd\u03bf" + }, + "connectivity": { + "off": "\u0391\u03c0\u03bf\u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7", + "on": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf\u03c2" + }, + "door": { + "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03ae", + "on": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03ae" + }, + "garage_door": { + "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", + "on": "\u0386\u03bd\u03bf\u03b9\u03b3\u03bc\u03b1" + }, + "gas": { + "off": "\u0394\u03b5\u03bd \u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5", + "on": "\u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5" + }, + "heat": { + "off": "\u03a6\u03c5\u03c3\u03b9\u03bf\u03bb\u03bf\u03b3\u03b9\u03ba\u03cc", + "on": "\u039a\u03b1\u03c5\u03c4\u03cc" + }, + "lock": { + "off": "\u039a\u03bb\u03b5\u03b9\u03b4\u03c9\u03bc\u03ad\u03bd\u03bf", + "on": "\u039e\u03b5\u03ba\u03bb\u03b5\u03af\u03b4\u03c9\u03c4\u03bf" + }, + "moisture": { + "off": "\u039e\u03b7\u03c1\u03cc", + "on": "\u03a5\u03b3\u03c1\u03cc" + }, + "motion": { + "off": "\u0394\u03b5\u03bd \u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5", + "on": "\u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5" + }, + "occupancy": { + "off": "\u0394\u03b5\u03bd \u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5", + "on": "\u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5" + }, + "opening": { + "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", + "on": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc" + }, + "presence": { + "off": "\u0395\u03ba\u03c4\u03cc\u03c2", + "on": "\u03a3\u03c0\u03af\u03c4\u03b9" + }, + "problem": { + "off": "\u0395\u03bd\u03c4\u03ac\u03be\u03b5\u03b9", + "on": "\u03a0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1" + }, + "safety": { + "off": "\u0391\u03c3\u03c6\u03b1\u03bb\u03ae\u03c2", + "on": "\u0391\u03bd\u03b1\u03c3\u03c6\u03b1\u03bb\u03ae\u03c2" + }, + "smoke": { + "off": "\u0394\u03b5\u03bd \u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5", + "on": "\u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5" + }, + "sound": { + "off": "\u0394\u03b5\u03bd \u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5", + "on": "\u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5" + }, + "vibration": { + "off": "\u0394\u03b5\u03bd \u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5", + "on": "\u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5" + }, + "window": { + "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", + "on": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc" + } + }, + "title": "\u0394\u03c5\u03b1\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/en.json new file mode 100644 index 00000000000..98c8a3a220a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/en.json @@ -0,0 +1,191 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} battery is low", + "is_cold": "{entity_name} is cold", + "is_connected": "{entity_name} is connected", + "is_gas": "{entity_name} is detecting gas", + "is_hot": "{entity_name} is hot", + "is_light": "{entity_name} is detecting light", + "is_locked": "{entity_name} is locked", + "is_moist": "{entity_name} is moist", + "is_motion": "{entity_name} is detecting motion", + "is_moving": "{entity_name} is moving", + "is_no_gas": "{entity_name} is not detecting gas", + "is_no_light": "{entity_name} is not detecting light", + "is_no_motion": "{entity_name} is not detecting motion", + "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_vibration": "{entity_name} is not detecting vibration", + "is_not_bat_low": "{entity_name} battery is normal", + "is_not_cold": "{entity_name} is not cold", + "is_not_connected": "{entity_name} is disconnected", + "is_not_hot": "{entity_name} is not hot", + "is_not_locked": "{entity_name} is unlocked", + "is_not_moist": "{entity_name} is dry", + "is_not_moving": "{entity_name} is not moving", + "is_not_occupied": "{entity_name} is not occupied", + "is_not_open": "{entity_name} is closed", + "is_not_plugged_in": "{entity_name} is unplugged", + "is_not_powered": "{entity_name} is not powered", + "is_not_present": "{entity_name} is not present", + "is_not_unsafe": "{entity_name} is safe", + "is_occupied": "{entity_name} is occupied", + "is_off": "{entity_name} is off", + "is_on": "{entity_name} is on", + "is_open": "{entity_name} is open", + "is_plugged_in": "{entity_name} is plugged in", + "is_powered": "{entity_name} is powered", + "is_present": "{entity_name} is present", + "is_problem": "{entity_name} is detecting problem", + "is_smoke": "{entity_name} is detecting smoke", + "is_sound": "{entity_name} is detecting sound", + "is_unsafe": "{entity_name} is unsafe", + "is_vibration": "{entity_name} is detecting vibration" + }, + "trigger_type": { + "bat_low": "{entity_name} battery low", + "cold": "{entity_name} became cold", + "connected": "{entity_name} connected", + "gas": "{entity_name} started detecting gas", + "hot": "{entity_name} became hot", + "light": "{entity_name} started detecting light", + "locked": "{entity_name} locked", + "moist": "{entity_name} became moist", + "motion": "{entity_name} started detecting motion", + "moving": "{entity_name} started moving", + "no_gas": "{entity_name} stopped detecting gas", + "no_light": "{entity_name} stopped detecting light", + "no_motion": "{entity_name} stopped detecting motion", + "no_problem": "{entity_name} stopped detecting problem", + "no_smoke": "{entity_name} stopped detecting smoke", + "no_sound": "{entity_name} stopped detecting sound", + "no_vibration": "{entity_name} stopped detecting vibration", + "not_bat_low": "{entity_name} battery normal", + "not_cold": "{entity_name} became not cold", + "not_connected": "{entity_name} disconnected", + "not_hot": "{entity_name} became not hot", + "not_locked": "{entity_name} unlocked", + "not_moist": "{entity_name} became dry", + "not_moving": "{entity_name} stopped moving", + "not_occupied": "{entity_name} became not occupied", + "not_opened": "{entity_name} closed", + "not_plugged_in": "{entity_name} unplugged", + "not_powered": "{entity_name} not powered", + "not_present": "{entity_name} not present", + "not_unsafe": "{entity_name} became safe", + "occupied": "{entity_name} became occupied", + "opened": "{entity_name} opened", + "plugged_in": "{entity_name} plugged in", + "powered": "{entity_name} powered", + "present": "{entity_name} present", + "problem": "{entity_name} started detecting problem", + "smoke": "{entity_name} started detecting smoke", + "sound": "{entity_name} started detecting sound", + "turned_off": "{entity_name} turned off", + "turned_on": "{entity_name} turned on", + "unsafe": "{entity_name} became unsafe", + "vibration": "{entity_name} started detecting vibration" + } + }, + "state": { + "_": { + "off": "Off", + "on": "On" + }, + "battery": { + "off": "Normal", + "on": "Low" + }, + "battery_charging": { + "off": "Not charging", + "on": "Charging" + }, + "cold": { + "off": "Normal", + "on": "Cold" + }, + "connectivity": { + "off": "Disconnected", + "on": "Connected" + }, + "door": { + "off": "Closed", + "on": "Open" + }, + "garage_door": { + "off": "Closed", + "on": "Open" + }, + "gas": { + "off": "Clear", + "on": "Detected" + }, + "heat": { + "off": "Normal", + "on": "Hot" + }, + "light": { + "off": "No light", + "on": "Light detected" + }, + "lock": { + "off": "Locked", + "on": "Unlocked" + }, + "moisture": { + "off": "Dry", + "on": "Wet" + }, + "motion": { + "off": "Clear", + "on": "Detected" + }, + "moving": { + "off": "Not moving", + "on": "Moving" + }, + "occupancy": { + "off": "Clear", + "on": "Detected" + }, + "opening": { + "off": "Closed", + "on": "Open" + }, + "plug": { + "off": "Unplugged", + "on": "Plugged in" + }, + "presence": { + "off": "Away", + "on": "Home" + }, + "problem": { + "off": "OK", + "on": "Problem" + }, + "safety": { + "off": "Safe", + "on": "Unsafe" + }, + "smoke": { + "off": "Clear", + "on": "Detected" + }, + "sound": { + "off": "Clear", + "on": "Detected" + }, + "vibration": { + "off": "Clear", + "on": "Detected" + }, + "window": { + "off": "Closed", + "on": "Open" + } + }, + "title": "Binary sensor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/es-419.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/es-419.json new file mode 100644 index 00000000000..d8cc4219097 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/es-419.json @@ -0,0 +1,175 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} la bater\u00eda est\u00e1 baja", + "is_cold": "{entity_name} est\u00e1 fr\u00edo", + "is_connected": "{entity_name} est\u00e1 conectado", + "is_gas": "{entity_name} est\u00e1 detectando gas", + "is_hot": "{entity_name} est\u00e1 caliente", + "is_light": "{entity_name} est\u00e1 detectando luz", + "is_locked": "{entity_name} est\u00e1 bloqueado", + "is_moist": "{entity_name} est\u00e1 h\u00famedo", + "is_motion": "{entity_name} est\u00e1 detectando movimiento", + "is_moving": "{entity_name} se est\u00e1 moviendo", + "is_no_gas": "{entity_name} no detecta gas", + "is_no_light": "{entity_name} no detecta luz", + "is_no_motion": "{entity_name} no detecta movimiento", + "is_no_problem": "{entity_name} no detecta el problema", + "is_no_smoke": "{entity_name} no detecta humo", + "is_no_sound": "{entity_name} no detecta sonido", + "is_no_vibration": "{entity_name} no detecta vibraciones", + "is_not_bat_low": "{entity_name} bater\u00eda est\u00e1 normal", + "is_not_cold": "{entity_name} no est\u00e1 fr\u00edo", + "is_not_connected": "{entity_name} est\u00e1 desconectado", + "is_not_hot": "{entity_name} no est\u00e1 caliente", + "is_not_locked": "{entity_name} est\u00e1 desbloqueado", + "is_not_moist": "{entity_name} est\u00e1 seco", + "is_not_moving": "{entity_name} no se mueve", + "is_not_occupied": "{entity_name} no est\u00e1 ocupado", + "is_not_open": "{entity_name} est\u00e1 cerrado", + "is_not_plugged_in": "{entity_name} est\u00e1 desconectado", + "is_not_powered": "{entity_name} no tiene encendido", + "is_not_present": "{entity_name} no est\u00e1 presente", + "is_not_unsafe": "{entity_name} es seguro", + "is_occupied": "{entity_name} est\u00e1 ocupado", + "is_off": "{entity_name} est\u00e1 apagado", + "is_on": "{entity_name} est\u00e1 encendido", + "is_open": "{entity_name} est\u00e1 abierto", + "is_plugged_in": "{entity_name} est\u00e1 enchufado", + "is_powered": "{entity_name} est\u00e1 encendido", + "is_present": "{entity_name} est\u00e1 presente", + "is_problem": "{entity_name} est\u00e1 detectando un problema", + "is_smoke": "{entity_name} est\u00e1 detectando humo", + "is_sound": "{entity_name} est\u00e1 detectando sonido", + "is_unsafe": "{entity_name} es inseguro", + "is_vibration": "{entity_name} est\u00e1 detectando vibraciones" + }, + "trigger_type": { + "bat_low": "{entity_name} bater\u00eda baja", + "cold": "{entity_name} se enfri\u00f3", + "connected": "{entity_name} conectado", + "gas": "{entity_name} comenz\u00f3 a detectar gas", + "hot": "{entity_name} se calent\u00f3", + "light": "{entity_name} comenz\u00f3 a detectar luz", + "locked": "{entity_name} bloqueado", + "moist": "{entity_name} se humedeci\u00f3", + "motion": "{entity_name} comenz\u00f3 a detectar movimiento", + "moving": "{entity_name} comenz\u00f3 a moverse", + "no_gas": "{entity_name} dej\u00f3 de detectar gas", + "no_light": "{entity_name} dej\u00f3 de detectar luz", + "no_motion": "{entity_name} dej\u00f3 de detectar movimiento", + "no_problem": "{entity_name} dej\u00f3 de detectar problemas", + "no_smoke": "{entity_name} dej\u00f3 de detectar humo", + "no_sound": "{entity_name} dej\u00f3 de detectar sonido", + "no_vibration": "{entity_name} dej\u00f3 de detectar vibraciones", + "not_bat_low": "{entity_name} bater\u00eda normal", + "not_cold": "{entity_name} no se enfri\u00f3", + "not_connected": "{entity_name} desconectado", + "not_hot": "{entity_name} no se calent\u00f3", + "not_locked": "{entity_name} desbloqueado", + "not_moist": "{entity_name} se sec\u00f3", + "not_moving": "{entity_name} dej\u00f3 de moverse", + "not_occupied": "{entity_name} se desocup\u00f3", + "not_opened": "{entity_name} cerrado", + "not_plugged_in": "{entity_name} desconectado", + "not_powered": "{entity_name} no encendido", + "not_present": "{entity_name} no presente", + "not_unsafe": "{entity_name} se volvi\u00f3 seguro", + "occupied": "{entity_name} se ocup\u00f3", + "opened": "{entity_name} abierto", + "plugged_in": "{entity_name} enchufado", + "powered": "{entity_name} encendido", + "present": "{entity_name} presente", + "problem": "{entity_name} comenz\u00f3 a detectar problemas", + "smoke": "{entity_name} comenz\u00f3 a detectar humo", + "sound": "{entity_name} comenz\u00f3 a detectar sonido", + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido", + "unsafe": "{entity_name} se volvi\u00f3 inseguro", + "vibration": "{entity_name} comenz\u00f3 a detectar vibraciones" + } + }, + "state": { + "_": { + "off": "Desactivado", + "on": "Encendido" + }, + "battery": { + "off": "Normal", + "on": "Baja" + }, + "cold": { + "off": "Normal", + "on": "Fr\u00edo" + }, + "connectivity": { + "off": "Desconectado", + "on": "Conectado" + }, + "door": { + "off": "Cerrada", + "on": "Abierta" + }, + "garage_door": { + "off": "Cerrada", + "on": "Abierta" + }, + "gas": { + "off": "Despejado", + "on": "Detectado" + }, + "heat": { + "off": "Normal", + "on": "Caliente" + }, + "lock": { + "off": "Bloqueado", + "on": "Desbloqueado" + }, + "moisture": { + "off": "Seco", + "on": "Humedo" + }, + "motion": { + "off": "Despejado", + "on": "Detectado" + }, + "occupancy": { + "off": "Despejado", + "on": "Detectado" + }, + "opening": { + "off": "Cerrado", + "on": "Abierto" + }, + "presence": { + "off": "Fuera de casa", + "on": "En Casa" + }, + "problem": { + "off": "OK", + "on": "Problema" + }, + "safety": { + "off": "Seguro", + "on": "Inseguro" + }, + "smoke": { + "off": "Despejado", + "on": "Detectado" + }, + "sound": { + "off": "Despejado", + "on": "Detectado" + }, + "vibration": { + "off": "Despejado", + "on": "Detectado" + }, + "window": { + "off": "Cerrada", + "on": "Abierta" + } + }, + "title": "Sensor binario" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/es.json new file mode 100644 index 00000000000..05fc002ecb0 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/es.json @@ -0,0 +1,191 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} la bater\u00eda est\u00e1 baja", + "is_cold": "{entity_name} est\u00e1 fr\u00edo", + "is_connected": "{entity_name} est\u00e1 conectado", + "is_gas": "{entity_name} est\u00e1 detectando gas", + "is_hot": "{entity_name} est\u00e1 caliente", + "is_light": "{entity_name} est\u00e1 detectando luz", + "is_locked": "{entity_name} est\u00e1 bloqueado", + "is_moist": "{entity_name} est\u00e1 h\u00famedo", + "is_motion": "{entity_name} est\u00e1 detectando movimiento", + "is_moving": "{entity_name} se est\u00e1 moviendo", + "is_no_gas": "{entity_name} no detecta gas", + "is_no_light": "{entity_name} no detecta la luz", + "is_no_motion": "{entity_name} no detecta movimiento", + "is_no_problem": "{entity_name} no detecta el problema", + "is_no_smoke": "{entity_name} no detecta humo", + "is_no_sound": "{entity_name} no detecta sonido", + "is_no_vibration": "{entity_name} no detecta vibraci\u00f3n", + "is_not_bat_low": "La bater\u00eda de {entity_name} es normal", + "is_not_cold": "{entity_name} no est\u00e1 fr\u00edo", + "is_not_connected": "{entity_name} est\u00e1 desconectado", + "is_not_hot": "{entity_name} no est\u00e1 caliente", + "is_not_locked": "{entity_name} est\u00e1 desbloqueado", + "is_not_moist": "{entity_name} est\u00e1 seco", + "is_not_moving": "{entity_name} no se mueve", + "is_not_occupied": "{entity_name} no est\u00e1 ocupado", + "is_not_open": "{entity_name} est\u00e1 cerrado", + "is_not_plugged_in": "{entity_name} est\u00e1 desconectado", + "is_not_powered": "{entity_name} no tiene alimentaci\u00f3n", + "is_not_present": "{entity_name} no est\u00e1 presente", + "is_not_unsafe": "{entity_name} es seguro", + "is_occupied": "{entity_name} est\u00e1 ocupado", + "is_off": "{entity_name} est\u00e1 apagado", + "is_on": "{entity_name} est\u00e1 activado", + "is_open": "{entity_name} est\u00e1 abierto", + "is_plugged_in": "{entity_name} est\u00e1 conectado", + "is_powered": "{entity_name} est\u00e1 activado", + "is_present": "{entity_name} est\u00e1 presente", + "is_problem": "{entity_name} est\u00e1 detectando un problema", + "is_smoke": "{entity_name} est\u00e1 detectando humo", + "is_sound": "{entity_name} est\u00e1 detectando sonido", + "is_unsafe": "{entity_name} no es seguro", + "is_vibration": "{entity_name} est\u00e1 detectando vibraciones" + }, + "trigger_type": { + "bat_low": "{entity_name} bater\u00eda baja", + "cold": "{entity_name} se enfri\u00f3", + "connected": "{entity_name} conectado", + "gas": "{entity_name} empez\u00f3 a detectar gas", + "hot": "{entity_name} se est\u00e1 calentando", + "light": "{entity_name} empez\u00f3 a detectar la luz", + "locked": "{entity_name} bloqueado", + "moist": "{entity_name} se humedece", + "motion": "{entity_name} comenz\u00f3 a detectar movimiento", + "moving": "{entity_name} empez\u00f3 a moverse", + "no_gas": "{entity_name} dej\u00f3 de detectar gas", + "no_light": "{entity_name} dej\u00f3 de detectar la luz", + "no_motion": "{entity_name} dej\u00f3 de detectar movimiento", + "no_problem": "{entity_name} dej\u00f3 de detectar el problema", + "no_smoke": "{entity_name} dej\u00f3 de detectar humo", + "no_sound": "{entity_name} dej\u00f3 de detectar sonido", + "no_vibration": "{entity_name} dej\u00f3 de detectar vibraci\u00f3n", + "not_bat_low": "{entity_name} bater\u00eda normal", + "not_cold": "{entity_name} no se enfri\u00f3", + "not_connected": "{entity_name} desconectado", + "not_hot": "{entity_name} no se calent\u00f3", + "not_locked": "{entity_name} desbloqueado", + "not_moist": "{entity_name} se sec\u00f3", + "not_moving": "{entity_name} dej\u00f3 de moverse", + "not_occupied": "{entity_name} no est\u00e1 ocupado", + "not_opened": "{entity_name} cerrado", + "not_plugged_in": "{entity_name} desconectado", + "not_powered": "{entity_name} no est\u00e1 activado", + "not_present": "{entity_name} no est\u00e1 presente", + "not_unsafe": "{entity_name} se volvi\u00f3 seguro", + "occupied": "{entity_name} se convirti\u00f3 en ocupado", + "opened": "{entity_name} abierto", + "plugged_in": "{entity_name} conectado", + "powered": "{entity_name} alimentado", + "present": "{entity_name} presente", + "problem": "{entity_name} empez\u00f3 a detectar problemas", + "smoke": "{entity_name} empez\u00f3 a detectar humo", + "sound": "{entity_name} empez\u00f3 a detectar sonido", + "turned_off": "{entity_name} desactivado", + "turned_on": "{entity_name} activado", + "unsafe": "{entity_name} se volvi\u00f3 inseguro", + "vibration": "{entity_name} empez\u00f3 a detectar vibraciones" + } + }, + "state": { + "_": { + "off": "Apagado", + "on": "Encendido" + }, + "battery": { + "off": "Normal", + "on": "Baja" + }, + "battery_charging": { + "off": "No est\u00e1 cargando", + "on": "Cargando" + }, + "cold": { + "off": "Normal", + "on": "Fr\u00edo" + }, + "connectivity": { + "off": "Desconectado", + "on": "Conectado" + }, + "door": { + "off": "Cerrada", + "on": "Abierta" + }, + "garage_door": { + "off": "Cerrada", + "on": "Abierta" + }, + "gas": { + "off": "No detectado", + "on": "Detectado" + }, + "heat": { + "off": "Normal", + "on": "Caliente" + }, + "light": { + "off": "Sin luz", + "on": "Luz detectada" + }, + "lock": { + "off": "Bloqueado", + "on": "Desbloqueado" + }, + "moisture": { + "off": "Seco", + "on": "H\u00famedo" + }, + "motion": { + "off": "No detectado", + "on": "Detectado" + }, + "moving": { + "off": "No se mueve", + "on": "En movimiento" + }, + "occupancy": { + "off": "No detectado", + "on": "Detectado" + }, + "opening": { + "off": "Cerrado", + "on": "Abierto" + }, + "plug": { + "off": "Desenchufado", + "on": "Enchufado" + }, + "presence": { + "off": "Fuera de casa", + "on": "En casa" + }, + "problem": { + "off": "OK", + "on": "Problema" + }, + "safety": { + "off": "Seguro", + "on": "Inseguro" + }, + "smoke": { + "off": "No detectado", + "on": "Detectado" + }, + "sound": { + "off": "No detectado", + "on": "Detectado" + }, + "vibration": { + "off": "No detectado", + "on": "Detectado" + }, + "window": { + "off": "Cerrada", + "on": "Abierta" + } + }, + "title": "Sensor binario" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/et.json new file mode 100644 index 00000000000..99fbec0b89e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/et.json @@ -0,0 +1,191 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} aku on t\u00fchjenemas", + "is_cold": "{entity_name} on k\u00fclm", + "is_connected": "{entity_name} on \u00fchendatud", + "is_gas": "{entity_name} tuvastab gaasi(leket)", + "is_hot": "{entity_name} on kuum", + "is_light": "{entity_name} tuvastab valgust", + "is_locked": "{entity_name} on lukustatud", + "is_moist": "{entity_name} on niiske", + "is_motion": "{entity_name} tuvastab liikumist", + "is_moving": "{entity_name} liigub", + "is_no_gas": "{entity_name} ei tuvasta gaasi(leket)", + "is_no_light": "{entity_name} ei tuvasta valgust", + "is_no_motion": "{entity_name} ei tuvasta liikumist", + "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_vibration": "{entity_name} ei tuvasta vibratsiooni", + "is_not_bat_low": "{entity_name} aku on laetud", + "is_not_cold": "{entity_name} ei ole k\u00fclm", + "is_not_connected": "{entity_name} pole \u00fchendatud", + "is_not_hot": "{entity_name} ei ole kuum", + "is_not_locked": "{entity_name} on lukustamata", + "is_not_moist": "{entity_name} on kuiv", + "is_not_moving": "{entity_name} liikumist ei tuvastatud", + "is_not_occupied": "{entity_name} pole h\u00f5ivatud", + "is_not_open": "{entity_name} on suletud", + "is_not_plugged_in": "{entity_name} on lahti \u00fchendatud", + "is_not_powered": "{entity_name} ei ole voolu all", + "is_not_present": "{entity_name} puudub", + "is_not_unsafe": "{entity_name} on turvaline", + "is_occupied": "{entity_name} on h\u00f5ivatud", + "is_off": "{entity_name} on v\u00e4lja l\u00fclitatud", + "is_on": "{entity_name} on sisse l\u00fclitatud", + "is_open": "{entity_name} on avatud", + "is_plugged_in": "{entity_name} on \u00fchendatud", + "is_powered": "{entity_name} on voolu all", + "is_present": "{entity_name} on saadaval", + "is_problem": "Olemil {entity_name} on probleem", + "is_smoke": "{entity_name} tuvastab suitsu", + "is_sound": "{entity_name} tuvastab heli", + "is_unsafe": "{entity_name} on ebaturvaline", + "is_vibration": "{entity_name} tuvastab vibratsiooni" + }, + "trigger_type": { + "bat_low": "{entity_name} aku hakkab t\u00fchjaks saama", + "cold": "{entity_name} muutus k\u00fclmaks", + "connected": "{entity_name} on \u00fchendatud", + "gas": "{entity_name} tuvastas gaasi(leket)", + "hot": "{entity_name} muutus kuumaks", + "light": "{entity_name} tuvastas valgust", + "locked": "{entity_name} on lukus", + "moist": "{entity_name} muutus niiskeks", + "motion": "{entity_name} tuvastas liikumist", + "moving": "{entity_name} hakkas liikuma", + "no_gas": "{entity_name} l\u00f5petas gaasi(lekke) tuvastamise", + "no_light": "{entity_name} l\u00f5petas valguse tuvastamise", + "no_motion": "{entity_name} l\u00f5petas liikumise tuvastamise", + "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_vibration": "{entity_name} l\u00f5petas vibratsiooni tuvastamise", + "not_bat_low": "{entity_name} aku on laetud", + "not_cold": "{entity_name} ei ole enam k\u00fclm", + "not_connected": "{entity_name} on lahti \u00fchendatud", + "not_hot": "{entity_name} ei ole enam kuum", + "not_locked": "{entity_name} on lukustamata", + "not_moist": "{entity_name} muutus kuivaks", + "not_moving": "{entity_name} liikumine peatus", + "not_occupied": "{entity_name} vabanes h\u00f5ivest", + "not_opened": "{entity_name} sulgus", + "not_plugged_in": "{entity_name} \u00fchendati vooluv\u00f5rgust v\u00e4lja", + "not_powered": "{entity_name} pole toidet", + "not_present": "{entity_name} puudub", + "not_unsafe": "{entity_name} muutus turvaliseks", + "occupied": "{entity_name} h\u00f5ivati", + "opened": "{entity_name} avanes", + "plugged_in": "{entity_name} \u00fchendati", + "powered": "{entity_name} l\u00fcltus voolu alla", + "present": "{entity_name} on saadaval", + "problem": "{entity_name} avastas probleemi", + "smoke": "{entity_name} tuvastas suitsu", + "sound": "{entity_name} tuvastas heli", + "turned_off": "{entity_name} l\u00fclitus v\u00e4lja", + "turned_on": "{entity_name} l\u00fclitus sisse", + "unsafe": "{entity_name} on ebaturvaline", + "vibration": "{entity_name} registreeris vibratsiooni" + } + }, + "state": { + "_": { + "off": "V\u00e4ljas", + "on": "Sees" + }, + "battery": { + "off": "Tavaline", + "on": "Madal" + }, + "battery_charging": { + "off": "Ei lae", + "on": "Laeb" + }, + "cold": { + "off": "Normaalne", + "on": "Jahe" + }, + "connectivity": { + "off": "Lahti \u00fchendatud", + "on": "\u00dchendatud" + }, + "door": { + "off": "Suletud", + "on": "Avatud" + }, + "garage_door": { + "off": "Suletud", + "on": "Avatud" + }, + "gas": { + "off": "Puudub", + "on": "Tuvastatud" + }, + "heat": { + "off": "Normaalne", + "on": "Palav" + }, + "light": { + "off": "Valgus puudub", + "on": "Valgus tuvastatud" + }, + "lock": { + "off": "Lukus", + "on": "Lukustamata" + }, + "moisture": { + "off": "Kuiv", + "on": "M\u00e4rg" + }, + "motion": { + "off": "Liikumine puudub", + "on": "Liikumine tuvastatud" + }, + "moving": { + "off": "Ei liigu", + "on": "Liigub" + }, + "occupancy": { + "off": "Puudub", + "on": "Tuvastatud" + }, + "opening": { + "off": "Suletud", + "on": "Avatud" + }, + "plug": { + "off": "Lahti \u00fchendatud", + "on": "\u00dchendatud" + }, + "presence": { + "off": "Eemal", + "on": "Kodus" + }, + "problem": { + "off": "OK", + "on": "Probleem" + }, + "safety": { + "off": "Ohutu", + "on": "Ohtlik" + }, + "smoke": { + "off": "Puudub", + "on": "Tuvastatud" + }, + "sound": { + "off": "Puudub", + "on": "Tuvastatud" + }, + "vibration": { + "off": "Puudub", + "on": "Tuvastatud" + }, + "window": { + "off": "Suletud", + "on": "Avatud" + } + }, + "title": "Binaarne andur" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/eu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/eu.json new file mode 100644 index 00000000000..a60728ce6cd --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/eu.json @@ -0,0 +1,60 @@ +{ + "state": { + "_": { + "off": "Itzalita", + "on": "Piztuta" + }, + "battery": { + "off": "Normala", + "on": "Baxua" + }, + "cold": { + "off": "Normala", + "on": "Hotza" + }, + "connectivity": { + "off": "Deskonektatuta", + "on": "Konektatuta" + }, + "door": { + "off": "Itxita", + "on": "Ireki" + }, + "garage_door": { + "off": "Itxita", + "on": "Ireki" + }, + "heat": { + "off": "Normala", + "on": "Beroa" + }, + "lock": { + "off": "Itxita", + "on": "Irekita" + }, + "moisture": { + "off": "Lehorra", + "on": "Buztita" + }, + "opening": { + "off": "Itxita", + "on": "Ireki" + }, + "presence": { + "off": "Kanpoan", + "on": "Etxean" + }, + "problem": { + "off": "Ondo", + "on": "Arazoa" + }, + "safety": { + "off": "Babestuta" + }, + "window": { + "off": "Itxita", + "on": "Ireki" + } + }, + "title": "Sentsore bitarra" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/fa.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/fa.json new file mode 100644 index 00000000000..4fbfa928fcd --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/fa.json @@ -0,0 +1,85 @@ +{ + "state": { + "_": { + "off": "\u062e\u0627\u0645\u0648\u0634", + "on": "\u0631\u0648\u0634\u0646" + }, + "battery": { + "off": "\u0639\u0627\u062f\u06cc", + "on": "\u06a9\u0645" + }, + "cold": { + "off": "\u0639\u0627\u062f\u06cc", + "on": "\u0633\u0631\u062f" + }, + "connectivity": { + "off": "\u0642\u0637\u0639 ", + "on": "\u0645\u062a\u0635\u0644" + }, + "door": { + "off": "\u0628\u0633\u062a\u0647", + "on": "\u0628\u0627\u0632" + }, + "garage_door": { + "off": "\u0628\u0633\u062a\u0647", + "on": "\u0628\u0627\u0632" + }, + "gas": { + "off": "\u0639\u0627\u062f\u06cc", + "on": "\u0634\u0646\u0627\u0633\u0627\u06cc\u06cc \u0634\u062f" + }, + "heat": { + "off": "\u0639\u0627\u062f\u06cc", + "on": "\u062f\u0627\u063a" + }, + "lock": { + "off": "\u0642\u0641\u0644", + "on": "\u0628\u0627\u0632" + }, + "moisture": { + "off": "\u062e\u0634\u06a9", + "on": "\u0645\u0631\u0637\u0648\u0628" + }, + "motion": { + "off": "\u0639\u0627\u062f\u06cc", + "on": "\u0634\u0646\u0627\u0633\u0627\u06cc\u06cc \u0634\u062f" + }, + "occupancy": { + "off": "\u0639\u0627\u062f\u06cc", + "on": "\u0634\u0646\u0627\u0633\u0627\u06cc\u06cc \u0634\u062f" + }, + "opening": { + "off": "\u0628\u0633\u062a\u0647 \u0634\u062f\u0647", + "on": "\u0628\u0627\u0632" + }, + "presence": { + "off": "\u0628\u06cc\u0631\u0648\u0646", + "on": "\u062e\u0627\u0646\u0647" + }, + "problem": { + "off": "\u062e\u0648\u0628", + "on": "\u0645\u0634\u06a9\u0644" + }, + "safety": { + "off": "\u0627\u0645\u0646", + "on": "\u0646\u0627 \u0627\u0645\u0646" + }, + "smoke": { + "off": "\u0639\u0627\u062f\u06cc", + "on": "\u0634\u0646\u0627\u0633\u0627\u06cc\u06cc \u0634\u062f" + }, + "sound": { + "off": "\u0639\u0627\u062f\u06cc", + "on": "\u0634\u0646\u0627\u0633\u0627\u06cc\u06cc \u0634\u062f" + }, + "vibration": { + "off": "\u067e\u0627\u06a9 \u06a9\u0631\u062f\u0646", + "on": "\u0634\u0646\u0627\u0633\u0627\u06cc\u06cc \u0634\u062f" + }, + "window": { + "off": "\u0628\u0633\u062a\u0647", + "on": "\u0628\u0627\u0632" + } + }, + "title": "\u062d\u0633\u06af\u0631 \u0628\u0627\u06cc\u0646\u0631\u06cc" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/fi.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/fi.json new file mode 100644 index 00000000000..b5c65028e73 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/fi.json @@ -0,0 +1,85 @@ +{ + "state": { + "_": { + "off": "Pois", + "on": "P\u00e4\u00e4ll\u00e4" + }, + "battery": { + "off": "Normaali", + "on": "Alhainen" + }, + "cold": { + "off": "Normaali", + "on": "Kylm\u00e4" + }, + "connectivity": { + "off": "Ei yhteytt\u00e4", + "on": "Yhdistetty" + }, + "door": { + "off": "Suljettu", + "on": "Auki" + }, + "garage_door": { + "off": "Suljettu", + "on": "Auki" + }, + "gas": { + "off": "Pois", + "on": "Havaittu" + }, + "heat": { + "off": "Normaali", + "on": "Kuuma" + }, + "lock": { + "off": "Lukittu", + "on": "Auki" + }, + "moisture": { + "off": "Kuiva", + "on": "Kostea" + }, + "motion": { + "off": "Ei liikett\u00e4", + "on": "Havaittu" + }, + "occupancy": { + "off": "Ei liikett\u00e4", + "on": "Havaittu" + }, + "opening": { + "off": "Suljettu", + "on": "Auki" + }, + "presence": { + "off": "Poissa", + "on": "Kotona" + }, + "problem": { + "off": "OK", + "on": "Ongelma" + }, + "safety": { + "off": "Turvallinen", + "on": "Vaarallinen" + }, + "smoke": { + "off": "Ei savua", + "on": "Havaittu" + }, + "sound": { + "off": "Ei \u00e4\u00e4nt\u00e4", + "on": "Havaittu" + }, + "vibration": { + "off": "Ei v\u00e4rin\u00e4\u00e4", + "on": "Havaittu" + }, + "window": { + "off": "Suljettu", + "on": "Auki" + } + }, + "title": "Bin\u00e4\u00e4risensori" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/fr.json new file mode 100644 index 00000000000..ede13a68dc9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/fr.json @@ -0,0 +1,191 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} batterie faible", + "is_cold": "{entity_name} est froid", + "is_connected": "{entity_name} est connect\u00e9", + "is_gas": "{entity_name} d\u00e9tecte du gaz", + "is_hot": "{entity_name} est chaud", + "is_light": "{entity_name} d\u00e9tecte de la lumi\u00e8re", + "is_locked": "{entity_name} est verrouill\u00e9", + "is_moist": "{entity_name} est humide", + "is_motion": "{entity_name} d\u00e9tecte du mouvement", + "is_moving": "{entity_name} se d\u00e9place", + "is_no_gas": "{entity_name} ne d\u00e9tecte pas de gaz", + "is_no_light": "{entity_name} ne d\u00e9tecte pas de lumi\u00e8re", + "is_no_motion": "{entity_name} ne d\u00e9tecte pas de mouvement", + "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_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", + "is_not_connected": "{entity_name} est d\u00e9connect\u00e9", + "is_not_hot": "{entity_name} n'est pas chaud", + "is_not_locked": "{entity_name} est d\u00e9verrouill\u00e9", + "is_not_moist": "{entity_name} est sec", + "is_not_moving": "{entity_name} ne bouge pas", + "is_not_occupied": "{entity_name} n'est pas occup\u00e9", + "is_not_open": "{entity_name} est ferm\u00e9", + "is_not_plugged_in": "{entity_name} est d\u00e9branch\u00e9", + "is_not_powered": "{entity_name} n'est pas aliment\u00e9", + "is_not_present": "{entity_name} n'est pas pr\u00e9sent", + "is_not_unsafe": "{entity_name} est en s\u00e9curit\u00e9", + "is_occupied": "{entity_name} est occup\u00e9", + "is_off": "{entity_name} est d\u00e9sactiv\u00e9", + "is_on": "{entity_name} est activ\u00e9", + "is_open": "{entity_name} est ouvert", + "is_plugged_in": "{entity_name} est branch\u00e9", + "is_powered": "{entity_name} est aliment\u00e9", + "is_present": "{entity_name} est pr\u00e9sent", + "is_problem": "{entity_name} d\u00e9tecte un probl\u00e8me", + "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_vibration": "{entity_name} d\u00e9tecte des vibrations" + }, + "trigger_type": { + "bat_low": "{entity_name} batterie faible", + "cold": "{entity_name} est devenu froid", + "connected": "{entity_name} connect\u00e9", + "gas": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter du gaz", + "hot": "{entity_name} est devenu chaud", + "light": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter la lumi\u00e8re", + "locked": "{entity_name} verrouill\u00e9", + "moist": "{entity_name} est devenu humide", + "motion": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter du mouvement", + "moving": "{entity_name} a commenc\u00e9 \u00e0 se d\u00e9placer", + "no_gas": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter le gaz", + "no_light": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter la lumi\u00e8re", + "no_motion": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter le mouvement", + "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_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", + "not_connected": "{entity_name} d\u00e9connect\u00e9", + "not_hot": "{entity_name} n'est plus chaud", + "not_locked": "{entity_name} d\u00e9verrouill\u00e9", + "not_moist": "{entity_name} est devenu sec", + "not_moving": "{entity_name} a cess\u00e9 de bouger", + "not_occupied": "{entity_name} est devenu non occup\u00e9", + "not_opened": "{entity_name} ferm\u00e9", + "not_plugged_in": "{entity_name} d\u00e9branch\u00e9", + "not_powered": "{entity_name} non aliment\u00e9", + "not_present": "{entity_name} non pr\u00e9sent", + "not_unsafe": "{entity_name} est devenu s\u00fbr", + "occupied": "{entity_name} est devenu occup\u00e9", + "opened": "{entity_name} ouvert", + "plugged_in": "{entity_name} branch\u00e9", + "powered": "{entity_name} aliment\u00e9", + "present": "{entity_name} pr\u00e9sent", + "problem": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter un probl\u00e8me", + "smoke": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter la fum\u00e9e", + "sound": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter le son", + "turned_off": "{entity_name} est d\u00e9sactiv\u00e9", + "turned_on": "{entity_name} est activ\u00e9", + "unsafe": "{entity_name} est devenu dangereux", + "vibration": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter les vibrations" + } + }, + "state": { + "_": { + "off": "Inactif", + "on": "Actif" + }, + "battery": { + "off": "Normal", + "on": "Faible" + }, + "battery_charging": { + "off": "Pas en charge", + "on": "En charge" + }, + "cold": { + "off": "Normale", + "on": "Froid" + }, + "connectivity": { + "off": "D\u00e9connect\u00e9", + "on": "Connect\u00e9" + }, + "door": { + "off": "Ferm\u00e9e", + "on": "Ouverte" + }, + "garage_door": { + "off": "Ferm\u00e9e", + "on": "Ouverte" + }, + "gas": { + "off": "Non d\u00e9tect\u00e9", + "on": "D\u00e9tect\u00e9" + }, + "heat": { + "off": "Normale", + "on": "Chaud" + }, + "light": { + "off": "Pas de lumi\u00e8re", + "on": "Lumi\u00e8re d\u00e9tect\u00e9e" + }, + "lock": { + "off": "Verrouill\u00e9", + "on": "D\u00e9verrouill\u00e9" + }, + "moisture": { + "off": "Sec", + "on": "Humide" + }, + "motion": { + "off": "RAS", + "on": "D\u00e9tect\u00e9" + }, + "moving": { + "off": "Immobile", + "on": "En mouvement" + }, + "occupancy": { + "off": "RAS", + "on": "D\u00e9tect\u00e9" + }, + "opening": { + "off": "Ferm\u00e9", + "on": "Ouvert" + }, + "plug": { + "off": "D\u00e9branch\u00e9", + "on": "Branch\u00e9" + }, + "presence": { + "off": "Absent", + "on": "Pr\u00e9sent" + }, + "problem": { + "off": "OK", + "on": "Probl\u00e8me" + }, + "safety": { + "off": "S\u00e9curis\u00e9", + "on": "Dangereux" + }, + "smoke": { + "off": "Non d\u00e9tect\u00e9", + "on": "D\u00e9tect\u00e9" + }, + "sound": { + "off": "Non d\u00e9tect\u00e9", + "on": "D\u00e9tect\u00e9" + }, + "vibration": { + "off": "RAS", + "on": "D\u00e9tect\u00e9e" + }, + "window": { + "off": "Ferm\u00e9e", + "on": "Ouverte" + } + }, + "title": "Capteur binaire" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/gsw.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/gsw.json new file mode 100644 index 00000000000..51fdfdd3cde --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/gsw.json @@ -0,0 +1,64 @@ +{ + "state": { + "_": { + "off": "Us", + "on": "Ah" + }, + "battery": { + "off": "Normau", + "on": "Nidrig" + }, + "connectivity": { + "off": "Trennt", + "on": "Verbunge" + }, + "gas": { + "off": "Frei", + "on": "Erk\u00e4nnt" + }, + "heat": { + "on": "Heiss" + }, + "moisture": { + "off": "Troch\u00e4", + "on": "Nass" + }, + "motion": { + "off": "Ok", + "on": "Erch\u00e4nt" + }, + "occupancy": { + "off": "Ok", + "on": "Erch\u00e4nt" + }, + "opening": { + "off": "Gschlos\u00e4", + "on": "Off\u00e4" + }, + "presence": { + "off": "Nid Dahei", + "on": "Dahei" + }, + "problem": { + "off": "OK", + "on": "Problem" + }, + "safety": { + "off": "Sicher", + "on": "Unsicher" + }, + "smoke": { + "off": "Ok", + "on": "Erch\u00e4nt" + }, + "sound": { + "off": "Ok", + "on": "Erch\u00e4nt" + }, + "vibration": { + "off": "Ok", + "on": "Erch\u00e4nt" + } + }, + "title": "Bin\u00e4re Sensor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/he.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/he.json new file mode 100644 index 00000000000..5f4fb949b34 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/he.json @@ -0,0 +1,95 @@ +{ + "device_automation": { + "condition_type": { + "is_cold": "{entity_name} \u05e7\u05e8", + "is_not_cold": "{entity_name} \u05dc\u05d0 \u05e7\u05e8" + }, + "trigger_type": { + "cold": "{entity_name} \u05e0\u05d4\u05d9\u05d4 \u05e7\u05e8", + "not_cold": "{entity_name} \u05e0\u05d4\u05d9\u05d4 \u05dc\u05d0 \u05e7\u05e8" + } + }, + "state": { + "_": { + "off": "\u05db\u05d1\u05d5\u05d9", + "on": "\u05d3\u05dc\u05d5\u05e7" + }, + "battery": { + "off": "\u05e0\u05d5\u05e8\u05de\u05dc\u05d9", + "on": "\u05e0\u05de\u05d5\u05da" + }, + "cold": { + "off": "\u05e8\u05d2\u05d9\u05dc", + "on": "\u05e7\u05b7\u05e8" + }, + "connectivity": { + "off": "\u05de\u05e0\u05d5\u05ea\u05e7", + "on": "\u05de\u05d7\u05d5\u05d1\u05e8" + }, + "door": { + "off": "\u05e1\u05d2\u05d5\u05e8\u05d4", + "on": "\u05e4\u05ea\u05d5\u05d7\u05d4" + }, + "garage_door": { + "off": "\u05e1\u05d2\u05d5\u05e8\u05d4", + "on": "\u05e4\u05ea\u05d5\u05d7\u05d4" + }, + "gas": { + "off": "\u05e0\u05e7\u05d9", + "on": "\u05d0\u05d5\u05ea\u05e8" + }, + "heat": { + "off": "\u05e8\u05d2\u05d9\u05dc", + "on": "\u05d7\u05dd" + }, + "lock": { + "off": "\u05e0\u05e2\u05d5\u05dc", + "on": "\u05dc\u05d0 \u05e0\u05e2\u05d5\u05dc" + }, + "moisture": { + "off": "\u05d9\u05d1\u05e9", + "on": "\u05e8\u05d8\u05d5\u05d1" + }, + "motion": { + "off": "\u05e0\u05e7\u05d9", + "on": "\u05d6\u05d5\u05d4\u05d4" + }, + "occupancy": { + "off": "\u05e0\u05e7\u05d9", + "on": "\u05d6\u05d5\u05d4\u05d4" + }, + "opening": { + "off": "\u05e1\u05d2\u05d5\u05e8", + "on": "\u05e4\u05ea\u05d5\u05d7" + }, + "presence": { + "off": "\u05dc\u05d0 \u05e0\u05d5\u05db\u05d7", + "on": "\u05e0\u05d5\u05db\u05d7" + }, + "problem": { + "off": "\u05ea\u05e7\u05d9\u05df", + "on": "\u05d1\u05e2\u05d9\u05d4" + }, + "safety": { + "off": "\u05d1\u05d8\u05d5\u05d7", + "on": "\u05dc\u05d0 \u05d1\u05d8\u05d5\u05d7" + }, + "smoke": { + "off": "\u05e0\u05e7\u05d9", + "on": "\u05d0\u05d5\u05ea\u05e8" + }, + "sound": { + "off": "\u05e0\u05e7\u05d9", + "on": "\u05d0\u05d5\u05ea\u05e8" + }, + "vibration": { + "off": "\u05e0\u05e7\u05d9", + "on": "\u05d0\u05d5\u05ea\u05e8" + }, + "window": { + "off": "\u05e1\u05d2\u05d5\u05e8", + "on": "\u05e4\u05ea\u05d5\u05d7" + } + }, + "title": "\u05d7\u05d9\u05d9\u05e9\u05df \u05d1\u05d9\u05e0\u05d0\u05e8\u05d9" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/hi.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/hi.json new file mode 100644 index 00000000000..ca66925b6c9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/hi.json @@ -0,0 +1,45 @@ +{ + "state": { + "_": { + "off": "\u092c\u0902\u0926" + }, + "battery": { + "off": "\u0938\u093e\u0927\u093e\u0930\u0923", + "on": "\u0915\u092e" + }, + "cold": { + "off": "\u0938\u093e\u0927\u093e\u0930\u0923", + "on": "\u0938\u0930\u094d\u0926\u0940" + }, + "connectivity": { + "off": "\u0921\u093f\u0938\u094d\u0915\u0928\u0947\u0915\u094d\u091f \u0915\u093f\u092f\u093e \u0917\u092f\u093e", + "on": "\u091c\u0941\u0921\u093c\u0947 \u0939\u0941\u090f" + }, + "door": { + "off": "\u092c\u0902\u0926", + "on": "\u0916\u0941\u0932\u093e" + }, + "garage_door": { + "off": "\u092c\u0902\u0926", + "on": "\u0916\u0941\u0932\u093e" + }, + "heat": { + "on": "\u0917\u0930\u094d\u092e" + }, + "motion": { + "off": "\u0935\u093f\u0936\u0926", + "on": "\u0905\u0928\u0941\u0938\u0928\u094d\u0927\u093e\u0928\u093f\u0924" + }, + "opening": { + "on": "\u0916\u0941\u0932\u093e" + }, + "presence": { + "on": "\u0918\u0930" + }, + "window": { + "off": "\u092c\u0902\u0926", + "on": "\u0916\u0941\u0932\u0940" + } + }, + "title": "\u092c\u093e\u0907\u0928\u0930\u0940 \u0938\u0947\u0902\u0938\u0930" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/hr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/hr.json new file mode 100644 index 00000000000..b1586d5e0f3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/hr.json @@ -0,0 +1,85 @@ +{ + "state": { + "_": { + "off": "Isklju\u010den", + "on": "Uklju\u010den" + }, + "battery": { + "off": "Normalno", + "on": "Prazna" + }, + "cold": { + "off": "Normalno", + "on": "Hladno" + }, + "connectivity": { + "off": "Nije spojen", + "on": "Spojen" + }, + "door": { + "off": "Zatvoreno", + "on": "Otvori" + }, + "garage_door": { + "off": "Zatvoren", + "on": "Otvoreno" + }, + "gas": { + "off": "\u010cisto", + "on": "Otkriveno" + }, + "heat": { + "off": "Normalno", + "on": "Vru\u0107e" + }, + "lock": { + "off": "Zaklju\u010dano", + "on": "Otklju\u010dano" + }, + "moisture": { + "off": "Suho", + "on": "Mokro" + }, + "motion": { + "off": "\u010cisto", + "on": "Otkriveno" + }, + "occupancy": { + "off": "\u010cisto", + "on": "Otkriveno" + }, + "opening": { + "off": "Zatvoreno", + "on": "Otvoreno" + }, + "presence": { + "off": "Odsutan", + "on": "Doma" + }, + "problem": { + "off": "OK", + "on": "Problem" + }, + "safety": { + "off": "Sigurno", + "on": "Nesigurno" + }, + "smoke": { + "off": "\u010cisto", + "on": "Otkriveno" + }, + "sound": { + "off": "\u010cisto", + "on": "Otkriveno" + }, + "vibration": { + "off": "\u010cisto", + "on": "Otkriveno" + }, + "window": { + "off": "Zatvoreno", + "on": "Otvoreno" + } + }, + "title": "Binarni senzor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/hu.json new file mode 100644 index 00000000000..c4395ca806c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/hu.json @@ -0,0 +1,191 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} akkufesz\u00fclts\u00e9g alacsony", + "is_cold": "{entity_name} hideg", + "is_connected": "{entity_name} csatlakoztatva van", + "is_gas": "{entity_name} g\u00e1zt \u00e9rz\u00e9kel", + "is_hot": "{entity_name} forr\u00f3", + "is_light": "{entity_name} f\u00e9nyt \u00e9rz\u00e9kel", + "is_locked": "{entity_name} z\u00e1rva van", + "is_moist": "{entity_name} nedves", + "is_motion": "{entity_name} mozg\u00e1st \u00e9rz\u00e9kel", + "is_moving": "{entity_name} mozog", + "is_no_gas": "{entity_name} nem \u00e9rz\u00e9kel g\u00e1zt", + "is_no_light": "{entity_name} nem \u00e9rz\u00e9kel f\u00e9nyt", + "is_no_motion": "{entity_name} nem \u00e9rz\u00e9kel mozg\u00e1st", + "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_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", + "is_not_connected": "{entity_name} le van csatlakoztatva", + "is_not_hot": "{entity_name} nem forr\u00f3", + "is_not_locked": "{entity_name} nyitva van", + "is_not_moist": "{entity_name} sz\u00e1raz", + "is_not_moving": "{entity_name} nem mozog", + "is_not_occupied": "{entity_name} nem foglalt", + "is_not_open": "{entity_name} z\u00e1rva van", + "is_not_plugged_in": "{entity_name} nincs csatlakoztatva", + "is_not_powered": "{entity_name} nincs fesz\u00fcts\u00e9g alatt", + "is_not_present": "{entity_name} nincs jelen", + "is_not_unsafe": "{entity_name} biztons\u00e1gos", + "is_occupied": "{entity_name} foglalt", + "is_off": "{entity_name} ki van kapcsolva", + "is_on": "{entity_name} be van kapcsolva", + "is_open": "{entity_name} nyitva van", + "is_plugged_in": "{entity_name} csatlakoztatva van", + "is_powered": "{entity_name} fesz\u00fclts\u00e9g alatt van", + "is_present": "{entity_name} jelen van", + "is_problem": "{entity_name} probl\u00e9m\u00e1t \u00e9szlel", + "is_smoke": "{entity_name} f\u00fcst\u00f6t \u00e9rz\u00e9kel", + "is_sound": "{entity_name} hangot \u00e9rz\u00e9kel", + "is_unsafe": "{entity_name} nem biztons\u00e1gos", + "is_vibration": "{entity_name} rezg\u00e9st \u00e9rz\u00e9kel" + }, + "trigger_type": { + "bat_low": "{entity_name} akkufesz\u00fclts\u00e9g alacsony", + "cold": "{entity_name} hideg lett", + "connected": "{entity_name} csatlakozik", + "gas": "{entity_name} g\u00e1zt \u00e9rz\u00e9kel", + "hot": "{entity_name} felforr\u00f3sodik", + "light": "{entity_name} f\u00e9nyt \u00e9rz\u00e9kel", + "locked": "{entity_name} be lett z\u00e1rva", + "moist": "{entity_name} nedves lett", + "motion": "{entity_name} mozg\u00e1st \u00e9rz\u00e9kel", + "moving": "{entity_name} mozog", + "no_gas": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel g\u00e1zt", + "no_light": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel f\u00e9nyt", + "no_motion": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel mozg\u00e1st", + "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_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", + "not_connected": "{entity_name} lecsatlakozik", + "not_hot": "{entity_name} m\u00e1r nem forr\u00f3", + "not_locked": "{entity_name} ki lett nyitva", + "not_moist": "{entity_name} sz\u00e1raz lett", + "not_moving": "{entity_name} m\u00e1r nem mozog", + "not_occupied": "{entity_name} m\u00e1r nem foglalt", + "not_opened": "{entity_name} be lett csukva", + "not_plugged_in": "{entity_name} m\u00e1r nincs csatlakoztatva", + "not_powered": "{entity_name} m\u00e1r nincs fesz\u00fcts\u00e9g alatt", + "not_present": "{entity_name} m\u00e1r nincs jelen", + "not_unsafe": "{entity_name} biztons\u00e1gos lett", + "occupied": "{entity_name} foglalt lett", + "opened": "{entity_name} ki lett nyitva", + "plugged_in": "{entity_name} csatlakoztatva lett", + "powered": "{entity_name} m\u00e1r fesz\u00fclts\u00e9g alatt van", + "present": "{entity_name} m\u00e1r jelen van", + "problem": "{entity_name} probl\u00e9m\u00e1t \u00e9szlel", + "smoke": "{entity_name} f\u00fcst\u00f6t \u00e9rz\u00e9kel", + "sound": "{entity_name} hangot \u00e9rz\u00e9kel", + "turned_off": "{entity_name} ki lett kapcsolva", + "turned_on": "{entity_name} be lett kapcsolva", + "unsafe": "{entity_name} m\u00e1r nem biztons\u00e1gos", + "vibration": "{entity_name} rezg\u00e9st \u00e9rz\u00e9kel" + } + }, + "state": { + "_": { + "off": "Ki", + "on": "Be" + }, + "battery": { + "off": "Norm\u00e1l", + "on": "Alacsony" + }, + "battery_charging": { + "off": "Nem t\u00f6lt\u0151dik", + "on": "T\u00f6lt\u0151dik" + }, + "cold": { + "off": "Norm\u00e1l", + "on": "Hideg" + }, + "connectivity": { + "off": "Lekapcsol\u00f3dva", + "on": "Kapcsol\u00f3dva" + }, + "door": { + "off": "Z\u00e1rva", + "on": "Nyitva" + }, + "garage_door": { + "off": "Z\u00e1rva", + "on": "Nyitva" + }, + "gas": { + "off": "Norm\u00e1l", + "on": "\u00c9szlelve" + }, + "heat": { + "off": "Norm\u00e1l", + "on": "Meleg" + }, + "light": { + "off": "Nincs f\u00e9ny", + "on": "F\u00e9ny \u00e9szlelve" + }, + "lock": { + "off": "Bez\u00e1rva", + "on": "Kinyitva" + }, + "moisture": { + "off": "Sz\u00e1raz", + "on": "Nedves" + }, + "motion": { + "off": "Norm\u00e1l", + "on": "\u00c9szlelve" + }, + "moving": { + "off": "Nincs mozg\u00e1sban", + "on": "Mozg\u00e1sban" + }, + "occupancy": { + "off": "Norm\u00e1l", + "on": "\u00c9szlelve" + }, + "opening": { + "off": "Z\u00e1rva", + "on": "Nyitva" + }, + "plug": { + "off": "Kih\u00fazva", + "on": "Bedugva" + }, + "presence": { + "off": "T\u00e1vol", + "on": "Otthon" + }, + "problem": { + "off": "OK", + "on": "Probl\u00e9ma" + }, + "safety": { + "off": "Biztons\u00e1gos", + "on": "Nem biztons\u00e1gos" + }, + "smoke": { + "off": "Norm\u00e1l", + "on": "\u00c9szlelve" + }, + "sound": { + "off": "Norm\u00e1l", + "on": "\u00c9szlelve" + }, + "vibration": { + "off": "Norm\u00e1l", + "on": "\u00c9szlelve" + }, + "window": { + "off": "Z\u00e1rva", + "on": "Nyitva" + } + }, + "title": "Bin\u00e1ris \u00e9rz\u00e9kel\u0151" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/hy.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/hy.json new file mode 100644 index 00000000000..7a23642b750 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/hy.json @@ -0,0 +1,85 @@ +{ + "state": { + "_": { + "off": "\u0531\u0576\u057b\u0561\u057f\u057e\u0561\u056e", + "on": "\u0544\u056b\u0561\u0581\u0561\u056e" + }, + "battery": { + "off": "\u0546\u0578\u0580\u0574\u0561\u056c \u0567", + "on": "\u0551\u0561\u056e\u0580" + }, + "cold": { + "off": "\u0546\u0578\u0580\u0574\u0561\u056c", + "on": "\u054d\u0561\u057c\u0568" + }, + "connectivity": { + "off": "\u0531\u0576\u057b\u0561\u057f\u057e\u0561\u056e \u0567", + "on": "\u053f\u0561\u057a\u057e\u0561\u056e" + }, + "door": { + "off": "\u0553\u0561\u056f\u057e\u0561\u056e \u0567", + "on": "\u0532\u0561\u0581\u0565\u056c" + }, + "garage_door": { + "off": "\u0553\u0561\u056f\u057e\u0561\u056e \u0567", + "on": "\u0532\u0561\u0581\u0565\u056c" + }, + "gas": { + "off": "\u0544\u0561\u0584\u0580\u0565\u056c", + "on": "\u0540\u0561\u0575\u057f\u0576\u0561\u0562\u0565\u0580\u057e\u0565\u056c \u0567" + }, + "heat": { + "off": "\u0546\u0578\u0580\u0574\u0561\u056c", + "on": "\u0539\u0565\u056a" + }, + "lock": { + "off": "\u056f\u0578\u0572\u057a\u057e\u0561\u056e", + "on": "\u0562\u0561\u0581\u0565\u056c \u0567" + }, + "moisture": { + "off": "\u0549\u0578\u0580", + "on": "\u053d\u0578\u0576\u0561\u057e" + }, + "motion": { + "off": "\u0544\u0561\u0584\u0580\u0565\u056c", + "on": "\u0540\u0561\u0575\u057f\u0576\u0561\u0562\u0565\u0580\u057e\u0565\u056c \u0567" + }, + "occupancy": { + "off": "\u0544\u0561\u0584\u0580\u0565\u056c", + "on": "\u0540\u0561\u0575\u057f\u0576\u0561\u0562\u0565\u0580\u057e\u0565\u056c \u0567" + }, + "opening": { + "off": "\u0553\u0561\u056f\u057e\u0561\u056e", + "on": "\u0532\u0561\u0581" + }, + "presence": { + "off": "\u0540\u0565\u057c\u0578\u0582", + "on": "\u054f\u0578\u0582\u0576" + }, + "problem": { + "off": "OK", + "on": "\u053d\u0576\u0564\u056b\u0580" + }, + "safety": { + "off": "\u0531\u057a\u0561\u0570\u0578\u057e", + "on": "\u0531\u0576\u057e\u057f\u0561\u0576\u0563" + }, + "smoke": { + "off": "\u0544\u0561\u0584\u0580\u0565\u056c", + "on": "\u0540\u0561\u0575\u057f\u0576\u0561\u0562\u0565\u0580\u057e\u0565\u056c \u0567" + }, + "sound": { + "off": "\u0544\u0561\u0584\u0580\u0565\u056c", + "on": "\u0540\u0561\u0575\u057f\u0576\u0561\u0562\u0565\u0580\u057e\u0565\u056c \u0567" + }, + "vibration": { + "off": "\u0544\u0561\u0584\u0580\u0565\u056c", + "on": "\u0540\u0561\u0575\u057f\u0576\u0561\u0562\u0565\u0580\u057e\u0565\u056c \u0567" + }, + "window": { + "off": "\u0553\u0561\u056f\u057e\u0561\u056e \u0567", + "on": "\u0532\u0561\u0581\u0565\u056c" + } + }, + "title": "\u0535\u0580\u056f\u0578\u0582\u0561\u056f\u0561\u0576 \u054d\u0565\u0576\u057d\u0578\u0580" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/id.json new file mode 100644 index 00000000000..ac880aa28fa --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/id.json @@ -0,0 +1,191 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "Baterai {entity_name} hampir habis", + "is_cold": "{entity_name} dingin", + "is_connected": "{entity_name} terhubung", + "is_gas": "{entity_name} mendeteksi gas", + "is_hot": "{entity_name} panas", + "is_light": "{entity_name} mendeteksi cahaya", + "is_locked": "{entity_name} terkunci", + "is_moist": "{entity_name} lembab", + "is_motion": "{entity_name} mendeteksi gerakan", + "is_moving": "{entity_name} bergerak", + "is_no_gas": "{entity_name} tidak mendeteksi gas", + "is_no_light": "{entity_name} tidak mendeteksi cahaya", + "is_no_motion": "{entity_name} tidak mendeteksi gerakan", + "is_no_problem": "{entity_name} tidak mendeteksi masalah", + "is_no_smoke": "{entity_name} tidak mendeteksi asap", + "is_no_sound": "{entity_name} tidak mendeteksi suara", + "is_no_vibration": "{entity_name} tidak mendeteksi getaran", + "is_not_bat_low": "Baterai {entity_name} normal", + "is_not_cold": "{entity_name} tidak dingin", + "is_not_connected": "{entity_name} terputus", + "is_not_hot": "{entity_name} tidak panas", + "is_not_locked": "{entity_name} tidak terkunci", + "is_not_moist": "{entity_name} kering", + "is_not_moving": "{entity_name} tidak bergerak", + "is_not_occupied": "{entity_name} tidak ditempati", + "is_not_open": "{entity_name} tertutup", + "is_not_plugged_in": "{entity_name} dicabut", + "is_not_powered": "{entity_name} tidak ditenagai", + "is_not_present": "{entity_name} tidak ada", + "is_not_unsafe": "{entity_name} aman", + "is_occupied": "{entity_name} ditempati", + "is_off": "{entity_name} mati", + "is_on": "{entity_name} nyala", + "is_open": "{entity_name} terbuka", + "is_plugged_in": "{entity_name} dicolokkan", + "is_powered": "{entity_name} ditenagai", + "is_present": "{entity_name} ada", + "is_problem": "{entity_name} mendeteksi masalah", + "is_smoke": "{entity_name} mendeteksi asap", + "is_sound": "{entity_name} mendeteksi suara", + "is_unsafe": "{entity_name} tidak aman", + "is_vibration": "{entity_name} mendeteksi getaran" + }, + "trigger_type": { + "bat_low": "Baterai {entity_name} hampir habis", + "cold": "{entity_name} menjadi dingin", + "connected": "{entity_name} terhubung", + "gas": "{entity_name} mulai mendeteksi gas", + "hot": "{entity_name} menjadi panas", + "light": "{entity_name} mulai mendeteksi cahaya", + "locked": "{entity_name} terkunci", + "moist": "{entity_name} menjadi lembab", + "motion": "{entity_name} mulai mendeteksi gerakan", + "moving": "{entity_name} mulai bergerak", + "no_gas": "{entity_name} berhenti mendeteksi gas", + "no_light": "{entity_name} berhenti mendeteksi cahaya", + "no_motion": "{entity_name} berhenti mendeteksi gerakan", + "no_problem": "{entity_name} berhenti mendeteksi masalah", + "no_smoke": "{entity_name} berhenti mendeteksi asap", + "no_sound": "{entity_name} berhenti mendeteksi suara", + "no_vibration": "{entity_name} berhenti mendeteksi getaran", + "not_bat_low": "Baterai {entity_name} normal", + "not_cold": "{entity_name} menjadi tidak dingin", + "not_connected": "{entity_name} terputus", + "not_hot": "{entity_name} menjadi tidak panas", + "not_locked": "{entity_name} tidak terkunci", + "not_moist": "{entity_name} menjadi kering", + "not_moving": "{entity_name} berhenti bergerak", + "not_occupied": "{entity_name} menjadi tidak ditempati", + "not_opened": "{entity_name} tertutup", + "not_plugged_in": "{entity_name} dicabut", + "not_powered": "{entity_name} tidak ditenagai", + "not_present": "{entity_name} tidak ada", + "not_unsafe": "{entity_name} menjadi aman", + "occupied": "{entity_name} menjadi ditempati", + "opened": "{entity_name} terbuka", + "plugged_in": "{entity_name} dicolokkan", + "powered": "{entity_name} ditenagai", + "present": "{entity_name} ada", + "problem": "{entity_name} mulai mendeteksi masalah", + "smoke": "{entity_name} mulai mendeteksi asap", + "sound": "{entity_name} mulai mendeteksi suara", + "turned_off": "{entity_name} dimatikan", + "turned_on": "{entity_name} dinyalakan", + "unsafe": "{entity_name} menjadi tidak aman", + "vibration": "{entity_name} mulai mendeteksi getaran" + } + }, + "state": { + "_": { + "off": "Mati", + "on": "Nyala" + }, + "battery": { + "off": "Normal", + "on": "Rendah" + }, + "battery_charging": { + "off": "Tidak mengisi daya", + "on": "Mengisi daya" + }, + "cold": { + "off": "Normal", + "on": "Dingin" + }, + "connectivity": { + "off": "Terputus", + "on": "Terhubung" + }, + "door": { + "off": "Tertutup", + "on": "Terbuka" + }, + "garage_door": { + "off": "Tertutup", + "on": "Terbuka" + }, + "gas": { + "off": "Tidak ada", + "on": "Terdeteksi" + }, + "heat": { + "off": "Normal", + "on": "Panas" + }, + "light": { + "off": "Tidak ada cahaya", + "on": "Cahaya terdeteksi" + }, + "lock": { + "off": "Terkunci", + "on": "Terbuka" + }, + "moisture": { + "off": "Kering", + "on": "Basah" + }, + "motion": { + "off": "Tidak ada", + "on": "Terdeteksi" + }, + "moving": { + "off": "Tidak bergerak", + "on": "Bergerak" + }, + "occupancy": { + "off": "Tidak ada", + "on": "Terdeteksi" + }, + "opening": { + "off": "Tertutup", + "on": "Terbuka" + }, + "plug": { + "off": "Dicabut", + "on": "Dicolokkan" + }, + "presence": { + "off": "Keluar", + "on": "Di Rumah" + }, + "problem": { + "off": "Oke", + "on": "Bermasalah" + }, + "safety": { + "off": "Aman", + "on": "Tidak aman" + }, + "smoke": { + "off": "Tidak ada", + "on": "Terdeteksi" + }, + "sound": { + "off": "Tidak ada", + "on": "Terdeteksi" + }, + "vibration": { + "off": "Tidak ada", + "on": "Terdeteksi" + }, + "window": { + "off": "Tertutup", + "on": "Terbuka" + } + }, + "title": "Sensor biner" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/is.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/is.json new file mode 100644 index 00000000000..f53316ebd73 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/is.json @@ -0,0 +1,80 @@ +{ + "state": { + "_": { + "off": "Sl\u00f6kkt", + "on": "Kveikt" + }, + "battery": { + "off": "Venjulegt", + "on": "L\u00e1gt" + }, + "cold": { + "off": "Venjulegt", + "on": "Kalt" + }, + "connectivity": { + "off": "Aftengdur", + "on": "Tengdur" + }, + "door": { + "off": "Loku\u00f0", + "on": "Opin" + }, + "garage_door": { + "off": "Loku\u00f0", + "on": "Opin" + }, + "gas": { + "off": "Hreinsa", + "on": "Uppg\u00f6tva\u00f0" + }, + "heat": { + "off": "Venjulegt", + "on": "Heitt" + }, + "lock": { + "off": "L\u00e6st", + "on": "Afl\u00e6st" + }, + "moisture": { + "off": "\u00deurrt", + "on": "Blautt" + }, + "motion": { + "off": "Engin hreyfing", + "on": "Hreyfing" + }, + "occupancy": { + "off": "Hreinsa", + "on": "Uppg\u00f6tva\u00f0" + }, + "presence": { + "off": "Fjarverandi", + "on": "Heima" + }, + "problem": { + "off": "\u00cd lagi", + "on": "Vandam\u00e1l" + }, + "safety": { + "off": "\u00d6ruggt", + "on": "\u00d3\u00f6ruggt" + }, + "smoke": { + "off": "Hreinsa", + "on": "Uppg\u00f6tva\u00f0" + }, + "sound": { + "off": "Hreinsa", + "on": "Uppg\u00f6tva\u00f0" + }, + "vibration": { + "on": "Uppg\u00f6tva\u00f0" + }, + "window": { + "off": "Loka", + "on": "Opna" + } + }, + "title": "Tv\u00edundar skynjari" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/it.json new file mode 100644 index 00000000000..68c427cbc04 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/it.json @@ -0,0 +1,191 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} la batteria \u00e8 scarica", + "is_cold": "{entity_name} \u00e8 freddo", + "is_connected": "{entity_name} \u00e8 collegato", + "is_gas": "{entity_name} sta rilevando il gas", + "is_hot": "{entity_name} \u00e8 caldo", + "is_light": "{entity_name} sta rilevando la luce", + "is_locked": "{entity_name} \u00e8 bloccato", + "is_moist": "{entity_name} \u00e8 umido", + "is_motion": "{entity_name} sta rilevando il movimento", + "is_moving": "{entity_name} si sta muovendo", + "is_no_gas": "{entity_name} non sta rilevando il gas", + "is_no_light": "{entity_name} non sta rilevando la luce", + "is_no_motion": "{entity_name} non sta rilevando il movimento", + "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_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", + "is_not_connected": "{entity_name} \u00e8 disconnesso", + "is_not_hot": "{entity_name} non \u00e8 caldo", + "is_not_locked": "{entity_name} \u00e8 sbloccato", + "is_not_moist": "{entity_name} \u00e8 asciutto", + "is_not_moving": "{entity_name} non si sta muovendo", + "is_not_occupied": "{entity_name} non \u00e8 occupato", + "is_not_open": "{entity_name} \u00e8 chiuso", + "is_not_plugged_in": "{entity_name} \u00e8 collegato", + "is_not_powered": "{entity_name} non \u00e8 alimentato", + "is_not_present": "{entity_name} non \u00e8 presente", + "is_not_unsafe": "{entity_name} \u00e8 sicuro", + "is_occupied": "{entity_name} \u00e8 occupato", + "is_off": "{entity_name} \u00e8 spento", + "is_on": "{entity_name} \u00e8 acceso", + "is_open": "{entity_name} \u00e8 aperto", + "is_plugged_in": "{entity_name} \u00e8 collegato", + "is_powered": "{entity_name} \u00e8 alimentato", + "is_present": "{entity_name} \u00e8 presente", + "is_problem": "{entity_name} sta rilevando un problema", + "is_smoke": "{entity_name} sta rilevando il fumo", + "is_sound": "{entity_name} sta rilevando il suono", + "is_unsafe": "{entity_name} non \u00e8 sicuro", + "is_vibration": "{entity_name} sta rilevando la vibrazione" + }, + "trigger_type": { + "bat_low": "{entity_name} batteria scarica", + "cold": "{entity_name} \u00e8 diventato freddo", + "connected": "{entity_name} connesso", + "gas": "{entity_name} ha iniziato a rilevare il gas", + "hot": "{entity_name} \u00e8 diventato caldo", + "light": "{entity_name} ha iniziato a rilevare la luce", + "locked": "{entity_name} bloccato", + "moist": "{entity_name} diventato umido", + "motion": "{entity_name} ha iniziato a rilevare il movimento", + "moving": "{entity_name} ha iniziato a muoversi", + "no_gas": "{entity_name} ha smesso la rilevazione di gas", + "no_light": "{entity_name} smesso il rilevamento di luce", + "no_motion": "{entity_name} ha smesso di rilevare il movimento", + "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_vibration": "{entity_name} ha smesso di rilevare le vibrazioni", + "not_bat_low": "{entity_name} batteria normale", + "not_cold": "{entity_name} non \u00e8 diventato freddo", + "not_connected": "{entity_name} \u00e8 disconnesso", + "not_hot": "{entity_name} non \u00e8 diventato caldo", + "not_locked": "{entity_name} \u00e8 sbloccato", + "not_moist": "{entity_name} \u00e8 diventato asciutto", + "not_moving": "{entity_name} ha smesso di muoversi", + "not_occupied": "{entity_name} non \u00e8 occupato", + "not_opened": "{entity_name} chiuso", + "not_plugged_in": "{entity_name} \u00e8 scollegato", + "not_powered": "{entity_name} non \u00e8 alimentato", + "not_present": "{entity_name} non \u00e8 presente", + "not_unsafe": "{entity_name} \u00e8 diventato sicuro", + "occupied": "{entity_name} \u00e8 diventato occupato", + "opened": "{entity_name} \u00e8 aperto", + "plugged_in": "{entity_name} \u00e8 collegato", + "powered": "{entity_name} \u00e8 alimentato", + "present": "{entity_name} \u00e8 presente", + "problem": "{entity_name} ha iniziato a rilevare un problema", + "smoke": "{entity_name} ha iniziato la rilevazione di fumo", + "sound": "{entity_name} ha iniziato il rilevamento del suono", + "turned_off": "{entity_name} disattivato", + "turned_on": "{entity_name} attivato", + "unsafe": "{entity_name} diventato non sicuro", + "vibration": "{entity_name} iniziato a rilevare le vibrazioni" + } + }, + "state": { + "_": { + "off": "Spento", + "on": "Acceso" + }, + "battery": { + "off": "Normale", + "on": "Basso" + }, + "battery_charging": { + "off": "Non in carica", + "on": "In carica" + }, + "cold": { + "off": "Normale", + "on": "Freddo" + }, + "connectivity": { + "off": "Disconnesso", + "on": "Connesso" + }, + "door": { + "off": "Chiusa", + "on": "Aperta" + }, + "garage_door": { + "off": "Chiusa", + "on": "Aperta" + }, + "gas": { + "off": "Assente", + "on": "Rilevato" + }, + "heat": { + "off": "Normale", + "on": "Caldo" + }, + "light": { + "off": "Nessuna luce", + "on": "Luce rilevata" + }, + "lock": { + "off": "Bloccato", + "on": "Sbloccato" + }, + "moisture": { + "off": "Asciutto", + "on": "Umido" + }, + "motion": { + "off": "Assente", + "on": "Rilevato" + }, + "moving": { + "off": "Non in movimento", + "on": "In movimento" + }, + "occupancy": { + "off": "Assente", + "on": "Rilevato" + }, + "opening": { + "off": "Chiuso", + "on": "Aperto" + }, + "plug": { + "off": "Scollegato", + "on": "Connesso" + }, + "presence": { + "off": "Fuori casa", + "on": "A casa" + }, + "problem": { + "off": "OK", + "on": "Problema" + }, + "safety": { + "off": "Sicuro", + "on": "Non Sicuro" + }, + "smoke": { + "off": "Assente", + "on": "Rilevato" + }, + "sound": { + "off": "Assente", + "on": "Rilevato" + }, + "vibration": { + "off": "Assente", + "on": "Rilevata" + }, + "window": { + "off": "Chiusa", + "on": "Aperta" + } + }, + "title": "Sensore binario" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ja.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ja.json new file mode 100644 index 00000000000..5434f8687bf --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ja.json @@ -0,0 +1,84 @@ +{ + "state": { + "_": { + "off": "\u30aa\u30d5", + "on": "\u30aa\u30f3" + }, + "battery": { + "off": "\u901a\u5e38", + "on": "\u4f4e" + }, + "cold": { + "off": "\u901a\u5e38", + "on": "\u4f4e\u6e29" + }, + "connectivity": { + "off": "\u5207\u65ad", + "on": "\u63a5\u7d9a\u6e08" + }, + "door": { + "off": "\u9589\u9396", + "on": "\u958b\u653e" + }, + "garage_door": { + "off": "\u9589\u9396", + "on": "\u958b\u653e" + }, + "gas": { + "off": "\u672a\u691c\u51fa", + "on": "\u691c\u51fa" + }, + "heat": { + "off": "\u6b63\u5e38", + "on": "\u9ad8\u6e29" + }, + "lock": { + "off": "\u30ed\u30c3\u30af\u3055\u308c\u307e\u3057\u305f", + "on": "\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + }, + "moisture": { + "off": "\u30c9\u30e9\u30a4", + "on": "\u30a6\u30a7\u30c3\u30c8" + }, + "motion": { + "off": "\u672a\u691c\u51fa", + "on": "\u691c\u51fa" + }, + "occupancy": { + "off": "\u672a\u691c\u51fa", + "on": "\u691c\u51fa" + }, + "opening": { + "off": "\u9589\u9396", + "on": "\u958b\u653e" + }, + "presence": { + "off": "\u5916\u51fa", + "on": "\u5728\u5b85" + }, + "problem": { + "off": "OK" + }, + "safety": { + "off": "\u5b89\u5168", + "on": "\u5371\u967a" + }, + "smoke": { + "off": "\u672a\u691c\u51fa", + "on": "\u691c\u51fa" + }, + "sound": { + "off": "\u672a\u691c\u51fa", + "on": "\u691c\u51fa" + }, + "vibration": { + "off": "\u672a\u691c\u51fa", + "on": "\u691c\u51fa" + }, + "window": { + "off": "\u9589\u9396", + "on": "\u958b\u653e" + } + }, + "title": "\u30d0\u30a4\u30ca\u30ea\u30bb\u30f3\u30b5\u30fc" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ka.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ka.json new file mode 100644 index 00000000000..25f9ea4e702 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ka.json @@ -0,0 +1,20 @@ +{ + "state": { + "battery_charging": { + "off": "\u10d0\u10e0 \u10d8\u10e2\u10d4\u10dc\u10d4\u10d1\u10d0", + "on": "\u10d8\u10e2\u10d4\u10dc\u10d4\u10d1\u10d0" + }, + "light": { + "off": "\u10e1\u10d8\u10dc\u10d0\u10d7\u10da\u10d4 \u1c90\u10e0 \u10d0\u10e0\u10d8\u10e1", + "on": "\u10d0\u10e6\u10db\u10dd\u10e9\u10d4\u10dc\u10d8\u10da\u10d8\u10d0 \u10e1\u10d8\u10dc\u10d0\u10d7\u10da\u10d4" + }, + "moving": { + "off": "\u10d0\u10e0 \u10db\u10dd\u10eb\u10e0\u10d0\u10dd\u10d1\u10e1", + "on": "\u10db\u10dd\u10eb\u10e0\u10d0\u10dd\u10d1\u10d0" + }, + "plug": { + "off": "\u10d2\u10d0\u10db\u10dd\u10d4\u10e0\u10d7\u10d4\u10d1\u10e3\u10da\u10d8", + "on": "\u1ca8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10e3\u10da\u10d8" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ko.json new file mode 100644 index 00000000000..7a725fc6719 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ko.json @@ -0,0 +1,191 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name}\uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud558\uba74", + "is_cold": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub0ae\uc73c\uba74", + "is_connected": "{entity_name}\uc774(\uac00) \uc5f0\uacb0\ub418\uc5b4 \uc788\uc73c\uba74", + "is_gas": "{entity_name}\uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc73c\uba74", + "is_hot": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub192\uc73c\uba74", + "is_light": "{entity_name}\uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uace0 \uc788\uc73c\uba74", + "is_locked": "{entity_name}\uc774(\uac00) \uc7a0\uaca8\uc788\uc73c\uba74", + "is_moist": "{entity_name}\uc774(\uac00) \uc2b5\ud558\uba74", + "is_motion": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uace0 \uc788\uc73c\uba74", + "is_moving": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc774\uace0 \uc788\uc73c\uba74", + "is_no_gas": "{entity_name}\uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74", + "is_no_light": "{entity_name}\uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74", + "is_no_motion": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74", + "is_no_problem": "{entity_name}\uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74", + "is_no_smoke": "{entity_name}\uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74", + "is_no_sound": "{entity_name}\uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74", + "is_no_vibration": "{entity_name}\uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74", + "is_not_bat_low": "{entity_name}\uc758 \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc774\uba74", + "is_not_cold": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub0ae\uc9c0 \uc54a\uc73c\uba74", + "is_not_connected": "{entity_name}\uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc838 \uc788\uc73c\uba74", + "is_not_hot": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub192\uc9c0 \uc54a\uc73c\uba74", + "is_not_locked": "{entity_name}\uc774(\uac00) \uc7a0\uaca8\uc788\uc9c0 \uc54a\uc73c\uba74", + "is_not_moist": "{entity_name}\uc774(\uac00) \uac74\uc870\ud558\uba74", + "is_not_moving": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc774\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74", + "is_not_occupied": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \uc544\ub2c8\uba74", + "is_not_open": "{entity_name}\uc774(\uac00) \ub2eb\ud600 \uc788\uc73c\uba74", + "is_not_plugged_in": "{entity_name}\uc758 \ud50c\ub7ec\uadf8\uac00 \ubf51\ud600 \uc788\uc73c\uba74", + "is_not_powered": "{entity_name}\uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74", + "is_not_present": "{entity_name}\uc774(\uac00) \uc678\ucd9c \uc911\uc774\uba74", + "is_not_unsafe": "{entity_name}\uc774(\uac00) \uc548\uc804\ud558\uba74", + "is_occupied": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uc774\uba74", + "is_off": "{entity_name}\uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", + "is_on": "{entity_name}\uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74", + "is_open": "{entity_name}\uc774(\uac00) \uc5f4\ub824 \uc788\uc73c\uba74", + "is_plugged_in": "{entity_name}\uc758 \ud50c\ub7ec\uadf8\uac00 \uaf3d\ud600 \uc788\uc73c\uba74", + "is_powered": "{entity_name}\uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uace0 \uc788\uc73c\uba74", + "is_present": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uc911\uc774\uba74", + "is_problem": "{entity_name}\uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc73c\uba74", + "is_smoke": "{entity_name}\uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc73c\uba74", + "is_sound": "{entity_name}\uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc73c\uba74", + "is_unsafe": "{entity_name}\uc774(\uac00) \uc548\uc804\ud558\uc9c0 \uc54a\uc73c\uba74", + "is_vibration": "{entity_name}\uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uace0 \uc788\uc73c\uba74" + }, + "trigger_type": { + "bat_low": "{entity_name}\uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud574\uc84c\uc744 \ub54c", + "cold": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub0ae\uc544\uc84c\uc744 \ub54c", + "connected": "{entity_name}\uc774(\uac00) \uc5f0\uacb0\ub418\uc5c8\uc744 \ub54c", + "gas": "{entity_name}\uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c", + "hot": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub192\uc544\uc84c\uc744 \ub54c", + "light": "{entity_name}\uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c", + "locked": "{entity_name}\uc774(\uac00) \uc7a0\uacbc\uc744 \ub54c", + "moist": "{entity_name}\uc774(\uac00) \uc2b5\ud574\uc84c\uc744 \ub54c", + "motion": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c", + "moving": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc774\uae30 \uc2dc\uc791\ud588\uc744 \ub54c", + "no_gas": "{entity_name}\uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "no_light": "{entity_name}\uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "no_motion": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "no_problem": "{entity_name}\uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "no_smoke": "{entity_name}\uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "no_sound": "{entity_name}\uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "no_vibration": "{entity_name}\uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "not_bat_low": "{entity_name}\uc758 \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc774 \ub418\uc5c8\uc744 \ub54c", + "not_cold": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub0ae\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "not_connected": "{entity_name}\uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc84c\uc744 \ub54c", + "not_hot": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub192\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "not_locked": "{entity_name}\uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub418\uc5c8\uc744 \ub54c", + "not_moist": "{entity_name}\uc774(\uac00) \uac74\uc870\ud574\uc84c\uc744 \ub54c", + "not_moving": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "not_occupied": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \uc544\ub2c8\uac8c \ub418\uc5c8\uc744 \ub54c", + "not_opened": "{entity_name}\uc774(\uac00) \ub2eb\ud614\uc744 \ub54c", + "not_plugged_in": "{entity_name}\uc758 \ud50c\ub7ec\uadf8\uac00 \ubf51\ud614\uc744 \ub54c", + "not_powered": "{entity_name}\uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "not_present": "{entity_name}\uc774(\uac00) \uc678\ucd9c \uc0c1\ud0dc\uac00 \ub418\uc5c8\uc744 \ub54c", + "not_unsafe": "{entity_name}\uc774(\uac00) \uc548\uc804\ud574\uc84c\uc744 \ub54c", + "occupied": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \ub418\uc5c8\uc744 \ub54c", + "opened": "{entity_name}\uc774(\uac00) \uc5f4\ub838\uc744 \ub54c", + "plugged_in": "{entity_name}\uc758 \ud50c\ub7ec\uadf8\uac00 \uaf3d\ud614\uc744 \ub54c", + "powered": "{entity_name}\uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc5c8\uc744 \ub54c", + "present": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \ub418\uc5c8\uc744 \ub54c", + "problem": "{entity_name}\uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c", + "smoke": "{entity_name}\uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c", + "sound": "{entity_name}\uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c", + "turned_off": "{entity_name}\uc774(\uac00) \uaebc\uc84c\uc744 \ub54c", + "turned_on": "{entity_name}\uc774(\uac00) \ucf1c\uc84c\uc744 \ub54c", + "unsafe": "{entity_name}\uc774(\uac00) \uc548\uc804\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "vibration": "{entity_name}\uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c" + } + }, + "state": { + "_": { + "off": "\uaebc\uc9d0", + "on": "\ucf1c\uc9d0" + }, + "battery": { + "off": "\ubcf4\ud1b5", + "on": "\ub0ae\uc74c" + }, + "battery_charging": { + "off": "\ucda9\uc804 \uc911\uc774 \uc544\ub2d8", + "on": "\ucda9\uc804 \uc911" + }, + "cold": { + "off": "\ubcf4\ud1b5", + "on": "\uc800\uc628" + }, + "connectivity": { + "off": "\uc5f0\uacb0\ud574\uc81c\ub428", + "on": "\uc5f0\uacb0\ub428" + }, + "door": { + "off": "\ub2eb\ud798", + "on": "\uc5f4\ub9bc" + }, + "garage_door": { + "off": "\ub2eb\ud798", + "on": "\uc5f4\ub9bc" + }, + "gas": { + "off": "\uc774\uc0c1\uc5c6\uc74c", + "on": "\uac10\uc9c0\ub428" + }, + "heat": { + "off": "\ubcf4\ud1b5", + "on": "\uace0\uc628" + }, + "light": { + "off": "\ube5b\uc774 \uc5c6\uc2b4", + "on": "\ube5b\uc744 \uac10\uc9c0\ud568" + }, + "lock": { + "off": "\uc7a0\uae40", + "on": "\ud574\uc81c" + }, + "moisture": { + "off": "\uac74\uc870\ud568", + "on": "\uc2b5\ud568" + }, + "motion": { + "off": "\uc774\uc0c1\uc5c6\uc74c", + "on": "\uac10\uc9c0\ub428" + }, + "moving": { + "off": "\uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc74c", + "on": "\uc6c0\uc9c1\uc784" + }, + "occupancy": { + "off": "\uc774\uc0c1\uc5c6\uc74c", + "on": "\uac10\uc9c0\ub428" + }, + "opening": { + "off": "\ub2eb\ud798", + "on": "\uc5f4\ub9bc" + }, + "plug": { + "off": "\ud50c\ub7ec\uadf8\uac00 \ubf51\ud798", + "on": "\ud50c\ub7ec\uadf8\uac00 \uaf3d\ud798" + }, + "presence": { + "off": "\uc678\ucd9c", + "on": "\uc7ac\uc2e4" + }, + "problem": { + "off": "\ubb38\uc81c\uc5c6\uc74c", + "on": "\ubb38\uc81c\uc788\uc74c" + }, + "safety": { + "off": "\uc548\uc804", + "on": "\uc704\ud5d8" + }, + "smoke": { + "off": "\uc774\uc0c1\uc5c6\uc74c", + "on": "\uac10\uc9c0\ub428" + }, + "sound": { + "off": "\uc774\uc0c1\uc5c6\uc74c", + "on": "\uac10\uc9c0\ub428" + }, + "vibration": { + "off": "\uc774\uc0c1\uc5c6\uc74c", + "on": "\uac10\uc9c0\ub428" + }, + "window": { + "off": "\ub2eb\ud798", + "on": "\uc5f4\ub9bc" + } + }, + "title": "\uc774\uc9c4\uc13c\uc11c" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/lb.json new file mode 100644 index 00000000000..fc186bee447 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/lb.json @@ -0,0 +1,187 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} Batterie ass niddereg", + "is_cold": "{entity_name} ass kal", + "is_connected": "{entity_name} ass verbonnen", + "is_gas": "{entity_name} entdeckt Gas", + "is_hot": "{entity_name} ass waarm", + "is_light": "{entity_name} entdeckt Luucht", + "is_locked": "{entity_name} ass gespaart", + "is_moist": "{entity_name} ass fiicht", + "is_motion": "{entity_name} entdeckt Beweegung", + "is_moving": "{entity_name} beweegt sech", + "is_no_gas": "{entity_name} entdeckt kee Gas", + "is_no_light": "{entity_name} entdeckt keng Luucht", + "is_no_motion": "{entity_name} entdeckt keng Beweegung", + "is_no_problem": "{entity_name} entdeckt keng Problemer", + "is_no_smoke": "{entity_name} entdeckt keen Damp", + "is_no_sound": "{entity_name} entdeckt keen Toun", + "is_no_vibration": "{entity_name} entdeckt keng Vibratiounen", + "is_not_bat_low": "{entity_name} Batterie ass normal", + "is_not_cold": "{entity_name} ass net kal", + "is_not_connected": "{entity_name} ass d\u00e9connect\u00e9iert", + "is_not_hot": "{entity_name} ass net waarm", + "is_not_locked": "{entity_name} ass entspaart", + "is_not_moist": "{entity_name} ass dr\u00e9chen", + "is_not_moving": "{entity_name} beweegt sech net", + "is_not_occupied": "{entity_name} ass fr\u00e4i", + "is_not_open": "{entity_name} ass zou", + "is_not_plugged_in": "{entity_name} ass net ugeschloss", + "is_not_powered": "{entity_name} ass net aliment\u00e9iert", + "is_not_present": "{entity_name} ass net pr\u00e4sent", + "is_not_unsafe": "{entity_name} ass s\u00e9cher", + "is_occupied": "{entity_name} ass besat", + "is_off": "{entity_name} ass aus", + "is_on": "{entity_name} ass un", + "is_open": "{entity_name} ass op", + "is_plugged_in": "{entity_name} ass ugeschloss", + "is_powered": "{entity_name} ass aliment\u00e9iert", + "is_present": "{entity_name} ass pr\u00e4sent", + "is_problem": "{entity_name} entdeckt Problemer", + "is_smoke": "{entity_name} entdeckt Damp", + "is_sound": "{entity_name} entdeckt Toun", + "is_unsafe": "{entity_name} ass ons\u00e9cher", + "is_vibration": "{entity_name} entdeckt Vibratiounen" + }, + "trigger_type": { + "bat_low": "{entity_name} Batterie niddereg", + "cold": "{entity_name} gouf kal", + "connected": "{entity_name} ass verbonnen", + "gas": "{entity_name} huet ugefaangen Gas z'entdecken", + "hot": "{entity_name} gouf waarm", + "light": "{entity_name} huet ugefange Luucht z'entdecken", + "locked": "{entity_name} gespaart", + "moist": "{entity_name} gouf fiicht", + "motion": "{entity_name} huet ugefaange Beweegung z'entdecken", + "moving": "{entity_name} huet ugefaangen sech ze beweegen", + "no_gas": "{entity_name} huet opgehale Gas z'entdecken", + "no_light": "{entity_name} huet opgehale Luucht z'entdecken", + "no_motion": "{entity_name} huet opgehale Beweegung z'entdecken", + "no_problem": "{entity_name} huet opgehale Problemer z'entdecken", + "no_smoke": "{entity_name} huet opgehale Damp z'entdecken", + "no_sound": "{entity_name} huet opgehale Toun z'entdecken", + "no_vibration": "{entity_name} huet opgehale Vibratiounen z'entdecken", + "not_bat_low": "{entity_name} Batterie normal", + "not_cold": "{entity_name} gouf net kal", + "not_connected": "{entity_name} d\u00e9connect\u00e9iert", + "not_hot": "{entity_name} gouf net waarm", + "not_locked": "{entity_name} entspaart", + "not_moist": "{entity_name} gouf dr\u00e9chen", + "not_moving": "{entity_name} huet opgehale sech ze beweegen", + "not_occupied": "{entity_name} gouf fr\u00e4i", + "not_opened": "{entity_name} gouf zougemaach", + "not_plugged_in": "{entity_name} net ugeschloss", + "not_powered": "{entity_name} net aliment\u00e9iert", + "not_present": "{entity_name} net pr\u00e4sent", + "not_unsafe": "{entity_name} gouf s\u00e9cher", + "occupied": "{entity_name} gouf besat", + "opened": "{entity_name} gouf opgemaach", + "plugged_in": "{entity_name} ugeschloss", + "powered": "{entity_name} aliment\u00e9iert", + "present": "{entity_name} pr\u00e4sent", + "problem": "{entity_name} huet ugefaange Problemer z'entdecken", + "smoke": "{entity_name} huet ugefaangen Damp z'entdecken", + "sound": "{entity_name} huet ugefaangen Toun z'entdecken", + "turned_off": "{entity_name} gouf ausgeschalt", + "turned_on": "{entity_name} gouf ugeschalt", + "unsafe": "{entity_name} gouf ons\u00e9cher", + "vibration": "{entity_name} huet ugefaange Vibratiounen z'entdecken" + } + }, + "state": { + "_": { + "off": "Aus", + "on": "Un" + }, + "battery": { + "off": "Normal", + "on": "Niddreg" + }, + "battery_charging": { + "off": "Lued net", + "on": "Lued" + }, + "cold": { + "off": "Normal", + "on": "Kal" + }, + "connectivity": { + "off": "Net Verbonnen", + "on": "Verbonnen" + }, + "door": { + "off": "Zou", + "on": "Op" + }, + "garage_door": { + "off": "Zou", + "on": "Op" + }, + "gas": { + "off": "Kloer", + "on": "Detekt\u00e9iert" + }, + "heat": { + "off": "Normal", + "on": "Waarm" + }, + "light": { + "off": "Keng Luucht", + "on": "Luucht detekt\u00e9iert" + }, + "lock": { + "off": "Gespaart", + "on": "Net gespaart" + }, + "moisture": { + "off": "Dr\u00e9chen", + "on": "Naass" + }, + "motion": { + "off": "Roueg", + "on": "Detekt\u00e9iert" + }, + "moving": { + "off": "Keng Beweegung", + "on": "Beweegung" + }, + "occupancy": { + "off": "Roueg", + "on": "Detekt\u00e9iert" + }, + "opening": { + "off": "Zou", + "on": "Op" + }, + "plug": { + "off": "Net ugeschloss", + "on": "Ugeschloss" + }, + "problem": { + "off": "OK", + "on": "Problem" + }, + "safety": { + "off": "S\u00e9cher", + "on": "Ons\u00e9cher" + }, + "smoke": { + "off": "Kloer", + "on": "Detekt\u00e9iert" + }, + "sound": { + "off": "Roueg", + "on": "Detekt\u00e9iert" + }, + "vibration": { + "off": "Kloer", + "on": "Detekt\u00e9iert" + }, + "window": { + "off": "Zou", + "on": "Op" + } + }, + "title": "Bin\u00e4ren Sensor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/lt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/lt.json new file mode 100644 index 00000000000..1214ac53470 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/lt.json @@ -0,0 +1,60 @@ +{ + "state": { + "_": { + "off": "I\u0161jungta", + "on": "\u012ejungta" + }, + "connectivity": { + "off": "Atsijung\u0119s", + "on": "Prisijung\u0119s" + }, + "door": { + "off": "U\u017edaryta", + "on": "Atidaryta" + }, + "garage_door": { + "off": "U\u017edaryta", + "on": "Atidaryta" + }, + "gas": { + "off": "Neaptikta", + "on": "Aptikta" + }, + "moisture": { + "off": "Sausa", + "on": "\u0160lapia" + }, + "motion": { + "off": "Nejuda", + "on": "Aptiktas judesys" + }, + "occupancy": { + "off": "Laisva", + "on": "U\u017eimta" + }, + "opening": { + "off": "U\u017edaryta", + "on": "Atidaryta" + }, + "safety": { + "off": "Saugu", + "on": "Nesaugu" + }, + "smoke": { + "off": "Neaptikta", + "on": "Aptikta" + }, + "sound": { + "off": "Tylu", + "on": "Aptikta" + }, + "vibration": { + "off": "Neaptikta", + "on": "Aptikta" + }, + "window": { + "off": "U\u017edaryta", + "on": "Atidaryta" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/lv.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/lv.json new file mode 100644 index 00000000000..14f39116c49 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/lv.json @@ -0,0 +1,91 @@ +{ + "device_automation": { + "trigger_type": { + "turned_off": "{entity_name} tika izsl\u0113gta", + "turned_on": "{entity_name} tika iesl\u0113gta" + } + }, + "state": { + "_": { + "off": "Izsl\u0113gts", + "on": "Iesl\u0113gts" + }, + "battery": { + "off": "Norm\u0101ls", + "on": "Zems" + }, + "cold": { + "off": "Norm\u0101ls", + "on": "Auksts" + }, + "connectivity": { + "off": "Atvienots", + "on": "Piesl\u0113dzies" + }, + "door": { + "off": "Aizv\u0113rtas", + "on": "Atv\u0113rtas" + }, + "garage_door": { + "off": "Aizv\u0113rtas", + "on": "Atv\u0113rtas" + }, + "gas": { + "off": "Br\u012bvs", + "on": "Sajusta" + }, + "heat": { + "off": "Norm\u0101ls", + "on": "Karsts" + }, + "lock": { + "off": "Sl\u0113gts", + "on": "Atsl\u0113gts" + }, + "moisture": { + "off": "Sauss", + "on": "Slapj\u0161" + }, + "motion": { + "off": "Br\u012bvs", + "on": "Sajusta" + }, + "occupancy": { + "off": "Br\u012bvs", + "on": "Aiz\u0146emts" + }, + "opening": { + "off": "Aizv\u0113rts", + "on": "Atv\u0113rts" + }, + "presence": { + "off": "Promb\u016btne", + "on": "M\u0101j\u0101s" + }, + "problem": { + "off": "OK", + "on": "Probl\u0113ma" + }, + "safety": { + "off": "Dro\u0161i", + "on": "Nedro\u0161i" + }, + "smoke": { + "off": "Br\u012bvs", + "on": "Sajusta" + }, + "sound": { + "off": "Br\u012bvs", + "on": "Sajusts" + }, + "vibration": { + "off": "Br\u012bvs", + "on": "Sajusts" + }, + "window": { + "off": "Aizv\u0113rts", + "on": "Atv\u0113rts" + } + }, + "title": "Bin\u0101rais sensors" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/nb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/nb.json new file mode 100644 index 00000000000..76c56713646 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/nb.json @@ -0,0 +1,85 @@ +{ + "state": { + "_": { + "off": "Av", + "on": "P\u00e5" + }, + "battery": { + "off": "Normalt", + "on": "Lavt" + }, + "cold": { + "off": "", + "on": "Kald" + }, + "connectivity": { + "off": "Frakoblet", + "on": "Tilkoblet" + }, + "door": { + "off": "Lukket", + "on": "\u00c5pen" + }, + "garage_door": { + "off": "Lukket", + "on": "\u00c5pen" + }, + "gas": { + "off": "Klar", + "on": "Oppdaget" + }, + "heat": { + "off": "Normal", + "on": "Varm" + }, + "lock": { + "off": "L\u00e5st", + "on": "Ul\u00e5st" + }, + "moisture": { + "off": "T\u00f8rr", + "on": "Fuktig" + }, + "motion": { + "off": "Klar", + "on": "Oppdaget" + }, + "occupancy": { + "off": "Klar", + "on": "Oppdaget" + }, + "opening": { + "off": "Lukket", + "on": "\u00c5pen" + }, + "presence": { + "off": "Borte", + "on": "Hjemme" + }, + "problem": { + "off": "", + "on": "" + }, + "safety": { + "off": "Sikker", + "on": "Usikker" + }, + "smoke": { + "off": "Klar", + "on": "Oppdaget" + }, + "sound": { + "off": "Klar", + "on": "Oppdaget" + }, + "vibration": { + "off": "Klar", + "on": "Oppdaget" + }, + "window": { + "off": "Lukket", + "on": "\u00c5pent" + } + }, + "title": "Bin\u00e6r sensor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/nl.json new file mode 100644 index 00000000000..9352bfa8d47 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/nl.json @@ -0,0 +1,191 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} batterij is bijna leeg", + "is_cold": "{entity_name} is koud", + "is_connected": "{entity_name} is verbonden", + "is_gas": "{entity_name} detecteert gas", + "is_hot": "{entity_name} is hot", + "is_light": "{entity_name} detecteert licht", + "is_locked": "{entity_name} is vergrendeld", + "is_moist": "{entity_name} is vochtig", + "is_motion": "{entity_name} detecteert beweging", + "is_moving": "{entity_name} is in beweging", + "is_no_gas": "{entity_name} detecteert geen gas", + "is_no_light": "{entity_name} detecteert geen licht", + "is_no_motion": "{entity_name} detecteert geen beweging", + "is_no_problem": "{entity_name} detecteert geen probleem", + "is_no_smoke": "{entity_name} detecteert geen rook", + "is_no_sound": "{entity_name} detecteert geen geluid", + "is_no_vibration": "{entity_name} detecteert geen trillingen", + "is_not_bat_low": "{entity_name} batterij is normaal", + "is_not_cold": "{entity_name} is niet koud", + "is_not_connected": "{entity_name} is niet verbonden", + "is_not_hot": "{entity_name} is niet heet", + "is_not_locked": "{entity_name} is ontgrendeld", + "is_not_moist": "{entity_name} is droog", + "is_not_moving": "{entity_name} beweegt niet", + "is_not_occupied": "{entity_name} is niet bezet", + "is_not_open": "{entity_name} is gesloten", + "is_not_plugged_in": "{entity_name} is niet aangesloten", + "is_not_powered": "{entity_name} is niet van stroom voorzien...", + "is_not_present": "{entity_name} is niet aanwezig", + "is_not_unsafe": "{entity_name} is veilig", + "is_occupied": "{entity_name} bezet is", + "is_off": "{entity_name} is uitgeschakeld", + "is_on": "{entity_name} is ingeschakeld", + "is_open": "{entity_name} is open", + "is_plugged_in": "{entity_name} is aangesloten", + "is_powered": "{entity_name} is van stroom voorzien....", + "is_present": "{entity_name} is aanwezig", + "is_problem": "{entity_name} detecteert een probleem", + "is_smoke": "{entity_name} detecteert rook", + "is_sound": "{entity_name} detecteert geluid", + "is_unsafe": "{entity_name} is onveilig", + "is_vibration": "{entity_name} detecteert trillingen" + }, + "trigger_type": { + "bat_low": "{entity_name} batterij bijna leeg", + "cold": "{entity_name} werd koud", + "connected": "{entity_name} verbonden", + "gas": "{entity_name} begon gas te detecteren", + "hot": "{entity_name} werd heet", + "light": "{entity_name} begon licht te detecteren", + "locked": "{entity_name} vergrendeld", + "moist": "{entity_name} werd vochtig", + "motion": "{entity_name} begon beweging te detecteren", + "moving": "{entity_name} begon te bewegen", + "no_gas": "{entity_name} is gestopt met het detecteren van gas", + "no_light": "{entity_name} gestopt met het detecteren van licht", + "no_motion": "{entity_name} is gestopt met het detecteren van beweging", + "no_problem": "{entity_name} gestopt met het detecteren van het probleem", + "no_smoke": "{entity_name} gestopt met het detecteren van rook", + "no_sound": "{entity_name} gestopt met het detecteren van geluid", + "no_vibration": "{entity_name} gestopt met het detecteren van trillingen", + "not_bat_low": "{entity_name} batterij normaal", + "not_cold": "{entity_name} werd niet koud", + "not_connected": "{entity_name} verbroken", + "not_hot": "{entity_name} werd niet warm", + "not_locked": "{entity_name} ontgrendeld", + "not_moist": "{entity_name} werd droog", + "not_moving": "{entity_name} gestopt met bewegen", + "not_occupied": "{entity_name} werd niet bezet", + "not_opened": "{entity_name} gesloten", + "not_plugged_in": "{entity_name} niet verbonden", + "not_powered": "{entity_name} niet ingeschakeld", + "not_present": "{entity_name} is niet aanwezig", + "not_unsafe": "{entity_name} werd veilig", + "occupied": "{entity_name} werd bezet", + "opened": "{entity_name} geopend", + "plugged_in": "{entity_name} aangesloten", + "powered": "{entity_name} heeft vermogen", + "present": "{entity_name} aanwezig", + "problem": "{entity_name} begonnen met het detecteren van een probleem", + "smoke": "{entity_name} begon rook te detecteren", + "sound": "{entity_name} begon geluid te detecteren", + "turned_off": "{entity_name} uitgeschakeld", + "turned_on": "{entity_name} ingeschakeld", + "unsafe": "{entity_name} werd onveilig", + "vibration": "{entity_name} begon trillingen te detecteren" + } + }, + "state": { + "_": { + "off": "Uit", + "on": "Aan" + }, + "battery": { + "off": "Normaal", + "on": "Laag" + }, + "battery_charging": { + "off": "Niet aan het opladen", + "on": "Opladen" + }, + "cold": { + "off": "Normaal", + "on": "Koud" + }, + "connectivity": { + "off": "Verbroken", + "on": "Verbonden" + }, + "door": { + "off": "Dicht", + "on": "Open" + }, + "garage_door": { + "off": "Dicht", + "on": "Open" + }, + "gas": { + "off": "Niet gedetecteerd", + "on": "Gedetecteerd" + }, + "heat": { + "off": "Normaal", + "on": "Heet" + }, + "light": { + "off": "Geen licht", + "on": "Licht gedetecteerd" + }, + "lock": { + "off": "Vergrendeld", + "on": "Ontgrendeld" + }, + "moisture": { + "off": "Droog", + "on": "Vochtig" + }, + "motion": { + "off": "Niet gedetecteerd", + "on": "Gedetecteerd" + }, + "moving": { + "off": "Niet bewegend", + "on": "In beweging" + }, + "occupancy": { + "off": "Niet gedetecteerd", + "on": "Gedetecteerd" + }, + "opening": { + "off": "Gesloten", + "on": "Open" + }, + "plug": { + "off": "Unplugged", + "on": "Ingeplugd" + }, + "presence": { + "off": "Afwezig", + "on": "Thuis" + }, + "problem": { + "off": "OK", + "on": "Probleem" + }, + "safety": { + "off": "Veilig", + "on": "Onveilig" + }, + "smoke": { + "off": "Niet gedetecteerd", + "on": "Gedetecteerd" + }, + "sound": { + "off": "Niet gedetecteerd", + "on": "Gedetecteerd" + }, + "vibration": { + "off": "Niet gedetecteerd", + "on": "Gedetecteerd" + }, + "window": { + "off": "Dicht", + "on": "Open" + } + }, + "title": "Binaire sensor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/nn.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/nn.json new file mode 100644 index 00000000000..740f55076f4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/nn.json @@ -0,0 +1,85 @@ +{ + "state": { + "_": { + "off": "Av", + "on": "P\u00e5" + }, + "battery": { + "off": "Normalt", + "on": "L\u00e5gt" + }, + "cold": { + "off": "Normal", + "on": "Kald" + }, + "connectivity": { + "off": "Fr\u00e5kopla", + "on": "Tilkopla" + }, + "door": { + "off": "Lukka", + "on": "Open" + }, + "garage_door": { + "off": "Lukka", + "on": "Open" + }, + "gas": { + "off": "Ikkje oppdaga", + "on": "Oppdaga" + }, + "heat": { + "off": "Normal", + "on": "Varm" + }, + "lock": { + "off": "L\u00e5st", + "on": "Ul\u00e5st" + }, + "moisture": { + "off": "T\u00f8rr", + "on": "V\u00e5t" + }, + "motion": { + "off": "Ikkje oppdaga", + "on": "Oppdaga" + }, + "occupancy": { + "off": "Ikkje oppdaga", + "on": "Oppdaga" + }, + "opening": { + "off": "Lukka", + "on": "Open" + }, + "presence": { + "off": "Borte", + "on": "Heime" + }, + "problem": { + "off": "Ok", + "on": "Problem" + }, + "safety": { + "off": "Sikker", + "on": "Usikker" + }, + "smoke": { + "off": "Ikkje oppdaga", + "on": "Oppdaga" + }, + "sound": { + "off": "Ikkje oppdaga", + "on": "Oppdaga" + }, + "vibration": { + "off": "Ikkje oppdaga", + "on": "Oppdaga" + }, + "window": { + "off": "Lukka", + "on": "Open" + } + }, + "title": "Bin\u00e6rsensor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/no.json new file mode 100644 index 00000000000..023fec6cc39 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/no.json @@ -0,0 +1,191 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} batteriniv\u00e5et er lavt", + "is_cold": "{entity_name} er kald", + "is_connected": "{entity_name} er tilkoblet", + "is_gas": "{entity_name} registrerer gass", + "is_hot": "{entity_name} er varm", + "is_light": "{entity_name} registrerer lys", + "is_locked": "{entity_name} er l\u00e5st", + "is_moist": "{entity_name} er fuktig", + "is_motion": "{entity_name} registrerer bevegelse", + "is_moving": "{entity_name} er i bevegelse", + "is_no_gas": "{entity_name} registrerer ikke gass", + "is_no_light": "{entity_name} registrerer ikke lys", + "is_no_motion": "{entity_name} registrerer ikke bevegelse", + "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_vibration": "{entity_name} registrerer ikke bevegelse", + "is_not_bat_low": "{entity_name} batteri er normalt", + "is_not_cold": "{entity_name} er ikke kald", + "is_not_connected": "{entity_name} er frakoblet", + "is_not_hot": "{entity_name} er ikke varm", + "is_not_locked": "{entity_name} er ul\u00e5st", + "is_not_moist": "{entity_name} er t\u00f8rr", + "is_not_moving": "{entity_name} er ikke i bevegelse", + "is_not_occupied": "{entity_name} er ledig", + "is_not_open": "{entity_name} er lukket", + "is_not_plugged_in": "{entity_name} er koblet fra", + "is_not_powered": "{entity_name} er spenningsl\u00f8s", + "is_not_present": "{entity_name} er ikke tilstede", + "is_not_unsafe": "{entity_name} er trygg", + "is_occupied": "{entity_name} er opptatt", + "is_off": "{entity_name} er sl\u00e5tt av", + "is_on": "{entity_name} er sl\u00e5tt p\u00e5", + "is_open": "{entity_name} er \u00e5pen", + "is_plugged_in": "{entity_name} er koblet til", + "is_powered": "{entity_name} er spenningssatt", + "is_present": "{entity_name} er tilstede", + "is_problem": "{entity_name} registrerer et problem", + "is_smoke": "{entity_name} registrerer r\u00f8yk", + "is_sound": "{entity_name} registrerer lyd", + "is_unsafe": "{entity_name} er utrygg", + "is_vibration": "{entity_name} registrerer vibrasjon" + }, + "trigger_type": { + "bat_low": "{entity_name} lavt batteri", + "cold": "{entity_name} ble kald", + "connected": "{entity_name} tilkoblet", + "gas": "{entity_name} begynte \u00e5 registrere gass", + "hot": "{entity_name} ble varm", + "light": "{entity_name} begynte \u00e5 registrere lys", + "locked": "{entity_name} l\u00e5st", + "moist": "{entity_name} ble fuktig", + "motion": "{entity_name} begynte \u00e5 registrere bevegelse", + "moving": "{entity_name} begynte \u00e5 bevege seg", + "no_gas": "{entity_name} sluttet \u00e5 registrere gass", + "no_light": "{entity_name} sluttet \u00e5 registrere lys", + "no_motion": "{entity_name} sluttet \u00e5 registrere bevegelse", + "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_vibration": "{entity_name} sluttet \u00e5 registrere vibrasjon", + "not_bat_low": "{entity_name} batteri normalt", + "not_cold": "{entity_name} ble ikke lenger kald", + "not_connected": "{entity_name} koblet fra", + "not_hot": "{entity_name} ble ikke lenger varm", + "not_locked": "{entity_name} l\u00e5st opp", + "not_moist": "{entity_name} ble t\u00f8rr", + "not_moving": "{entity_name} sluttet \u00e5 bevege seg", + "not_occupied": "{entity_name} ble ledig", + "not_opened": "{entity_name} stengt", + "not_plugged_in": "{entity_name} koblet fra", + "not_powered": "{entity_name} spenningsl\u00f8s", + "not_present": "{entity_name} ikke til stede", + "not_unsafe": "{entity_name} ble trygg", + "occupied": "{entity_name} ble opptatt", + "opened": "{entity_name} \u00e5pnet", + "plugged_in": "{entity_name} koblet til", + "powered": "{entity_name} spenningssatt", + "present": "{entity_name} tilstede", + "problem": "{entity_name} begynte \u00e5 registrere et problem", + "smoke": "{entity_name} begynte \u00e5 registrere r\u00f8yk", + "sound": "{entity_name} begynte \u00e5 registrere lyd", + "turned_off": "{entity_name} sl\u00e5tt av", + "turned_on": "{entity_name} sl\u00e5tt p\u00e5", + "unsafe": "{entity_name} ble usikker", + "vibration": "{entity_name} begynte \u00e5 oppdage vibrasjon" + } + }, + "state": { + "_": { + "off": "Av", + "on": "P\u00e5" + }, + "battery": { + "off": "Normal", + "on": "Lavt" + }, + "battery_charging": { + "off": "Lader ikke", + "on": "Lader" + }, + "cold": { + "off": "Normal", + "on": "Kald" + }, + "connectivity": { + "off": "Frakoblet", + "on": "Tilkoblet" + }, + "door": { + "off": "Lukket", + "on": "\u00c5pen" + }, + "garage_door": { + "off": "Lukket", + "on": "\u00c5pen" + }, + "gas": { + "off": "Klart", + "on": "Oppdaget" + }, + "heat": { + "off": "Normal", + "on": "Varm" + }, + "light": { + "off": "Ingen lys", + "on": "Lys oppdaget" + }, + "lock": { + "off": "L\u00e5st", + "on": "Ul\u00e5st" + }, + "moisture": { + "off": "T\u00f8rr", + "on": "V\u00e5t" + }, + "motion": { + "off": "Klart", + "on": "Oppdaget" + }, + "moving": { + "off": "Beveger seg ikke", + "on": "Flytter" + }, + "occupancy": { + "off": "Klart", + "on": "Oppdaget" + }, + "opening": { + "off": "Lukket", + "on": "\u00c5pen" + }, + "plug": { + "off": "Frakoblet", + "on": "Koblet til" + }, + "presence": { + "off": "Borte", + "on": "Hjemme" + }, + "problem": { + "off": "", + "on": "" + }, + "safety": { + "off": "Sikker", + "on": "Usikker" + }, + "smoke": { + "off": "Klart", + "on": "Oppdaget" + }, + "sound": { + "off": "Klart", + "on": "Oppdaget" + }, + "vibration": { + "off": "Klart", + "on": "Oppdaget" + }, + "window": { + "off": "Lukket", + "on": "\u00c5pent" + } + }, + "title": "Bin\u00e6r sensor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/pl.json new file mode 100644 index 00000000000..726765aea02 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/pl.json @@ -0,0 +1,191 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "bateria {entity_name} jest roz\u0142adowana", + "is_cold": "sensor {entity_name} wykrywa zimno", + "is_connected": "sensor {entity_name} raportuje po\u0142\u0105czenie", + "is_gas": "sensor {entity_name} wykrywa gaz", + "is_hot": "sensor {entity_name} wykrywa gor\u0105co", + "is_light": "sensor {entity_name} wykrywa \u015bwiat\u0142o", + "is_locked": "sensor {entity_name} wykrywa zamkni\u0119cie", + "is_moist": "sensor {entity_name} wykrywa wilgo\u0107", + "is_motion": "sensor {entity_name} wykrywa ruch", + "is_moving": "sensor {entity_name} porusza si\u0119", + "is_no_gas": "sensor {entity_name} nie wykrywa gazu", + "is_no_light": "sensor {entity_name} nie wykrywa \u015bwiat\u0142a", + "is_no_motion": "sensor {entity_name} nie wykrywa ruchu", + "is_no_problem": "sensor {entity_name} nie wykrywa problemu", + "is_no_smoke": "sensor {entity_name} nie wykrywa dymu", + "is_no_sound": "sensor {entity_name} nie wykrywa d\u017awi\u0119ku", + "is_no_vibration": "sensor {entity_name} nie wykrywa wibracji", + "is_not_bat_low": "bateria {entity_name} nie jest roz\u0142adowana", + "is_not_cold": "sensor {entity_name} nie wykrywa zimna", + "is_not_connected": "sensor {entity_name} nie wykrywa roz\u0142\u0105czenia", + "is_not_hot": "sensor {entity_name} nie wykrywa gor\u0105ca", + "is_not_locked": "sensor {entity_name} nie wykrywa otwarcia", + "is_not_moist": "sensor {entity_name} nie wykrywa wilgoci", + "is_not_moving": "sensor {entity_name} nie porusza si\u0119", + "is_not_occupied": "sensor {entity_name} nie jest zaj\u0119ty", + "is_not_open": "sensor {entity_name} jest zamkni\u0119ty", + "is_not_plugged_in": "sensor {entity_name} wykrywa od\u0142\u0105czenie", + "is_not_powered": "sensor {entity_name} nie wykrywa zasilania", + "is_not_present": "sensor {entity_name} nie wykrywa obecno\u015bci", + "is_not_unsafe": "sensor {entity_name} nie wykrywa zagro\u017cenia", + "is_occupied": "sensor {entity_name} jest zaj\u0119ty", + "is_off": "sensor {entity_name} jest wy\u0142\u0105czony", + "is_on": "sensor {entity_name} jest w\u0142\u0105czony", + "is_open": "sensor {entity_name} jest otwarty", + "is_plugged_in": "sensor {entity_name} wykrywa pod\u0142\u0105czenie", + "is_powered": "sensor {entity_name} wykrywa zasilanie", + "is_present": "sensor {entity_name} wykrywa obecno\u015b\u0107", + "is_problem": "sensor {entity_name} wykrywa problem", + "is_smoke": "sensor {entity_name} wykrywa dym", + "is_sound": "sensor {entity_name} wykrywa d\u017awi\u0119k", + "is_unsafe": "sensor {entity_name} wykrywa zagro\u017cenie", + "is_vibration": "sensor {entity_name} wykrywa wibracje" + }, + "trigger_type": { + "bat_low": "nast\u0105pi roz\u0142adowanie baterii {entity_name}", + "cold": "sensor {entity_name} wykryje zimno", + "connected": "nast\u0105pi pod\u0142\u0105czenie {entity_name}", + "gas": "sensor {entity_name} wykryje gaz", + "hot": "sensor {entity_name} wykryje gor\u0105co", + "light": "sensor {entity_name} wykryje \u015bwiat\u0142o", + "locked": "nast\u0105pi zamkni\u0119cie {entity_name}", + "moist": "nast\u0105pi wykrycie wilgoci {entity_name}", + "motion": "sensor {entity_name} wykryje ruch", + "moving": "sensor {entity_name} zacznie porusza\u0107 si\u0119", + "no_gas": "sensor {entity_name} przestanie wykrywa\u0107 gaz", + "no_light": "sensor {entity_name} przestanie wykrywa\u0107 \u015bwiat\u0142o", + "no_motion": "sensor {entity_name} przestanie wykrywa\u0107 ruch", + "no_problem": "sensor {entity_name} przestanie wykrywa\u0107 problem", + "no_smoke": "sensor {entity_name} przestanie wykrywa\u0107 dym", + "no_sound": "sensor {entity_name} przestanie wykrywa\u0107 d\u017awi\u0119k", + "no_vibration": "sensor {entity_name} przestanie wykrywa\u0107 wibracje", + "not_bat_low": "nast\u0105pi na\u0142adowanie baterii {entity_name}", + "not_cold": "sensor {entity_name} przestanie wykrywa\u0107 zimno", + "not_connected": "nast\u0105pi roz\u0142\u0105czenie {entity_name}", + "not_hot": "sensor {entity_name} przestanie wykrywa\u0107 gor\u0105co", + "not_locked": "nast\u0105pi otwarcie {entity_name}", + "not_moist": "sensor {entity_name} przestanie wykrywa\u0107 wilgo\u0107", + "not_moving": "sensor {entity_name} przestanie porusza\u0107 si\u0119", + "not_occupied": "sensor {entity_name} przestanie by\u0107 zaj\u0119ty", + "not_opened": "nast\u0105pi zamkni\u0119cie {entity_name}", + "not_plugged_in": "nast\u0105pi od\u0142\u0105czenie {entity_name}", + "not_powered": "nast\u0105pi od\u0142\u0105czenie zasilania {entity_name}", + "not_present": "sensor {entity_name} przestanie wykrywa\u0107 obecno\u015b\u0107", + "not_unsafe": "sensor {entity_name} przestanie wykrywa\u0107 zagro\u017cenie", + "occupied": "sensor {entity_name} stanie si\u0119 zaj\u0119ty", + "opened": "nast\u0105pi otwarcie {entity_name}", + "plugged_in": "nast\u0105pi pod\u0142\u0105czenie {entity_name}", + "powered": "nast\u0105pi pod\u0142\u0105czenie zasilenia {entity_name}", + "present": "sensor {entity_name} wykryje obecno\u015b\u0107", + "problem": "sensor {entity_name} wykryje problem", + "smoke": "sensor {entity_name} wykryje dym", + "sound": "sensor {entity_name} wykryje d\u017awi\u0119k", + "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", + "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}", + "unsafe": "sensor {entity_name} wykryje zagro\u017cenie", + "vibration": "sensor {entity_name} wykryje wibracje" + } + }, + "state": { + "_": { + "off": "wy\u0142.", + "on": "w\u0142." + }, + "battery": { + "off": "na\u0142adowana", + "on": "roz\u0142adowana" + }, + "battery_charging": { + "off": "roz\u0142adowywanie", + "on": "\u0142adowanie" + }, + "cold": { + "off": "normalnie", + "on": "zimno" + }, + "connectivity": { + "off": "offline", + "on": "online" + }, + "door": { + "off": "zamkni\u0119te", + "on": "otwarte" + }, + "garage_door": { + "off": "zamkni\u0119ta", + "on": "otwarta" + }, + "gas": { + "off": "brak", + "on": "wykryto" + }, + "heat": { + "off": "normalnie", + "on": "gor\u0105co" + }, + "light": { + "off": "brak", + "on": "wykryto" + }, + "lock": { + "off": "zamkni\u0119ty", + "on": "otwarty" + }, + "moisture": { + "off": "brak wilgoci", + "on": "wilgo\u0107" + }, + "motion": { + "off": "brak", + "on": "wykryto" + }, + "moving": { + "off": "brak ruchu", + "on": "w ruchu" + }, + "occupancy": { + "off": "brak", + "on": "wykryto" + }, + "opening": { + "off": "zamkni\u0119te", + "on": "otwarte" + }, + "plug": { + "off": "od\u0142\u0105czony", + "on": "pod\u0142\u0105czony" + }, + "presence": { + "off": "poza domem", + "on": "w domu" + }, + "problem": { + "off": "ok", + "on": "problem" + }, + "safety": { + "off": "brak zagro\u017cenia", + "on": "zagro\u017cenie" + }, + "smoke": { + "off": "brak", + "on": "wykryto" + }, + "sound": { + "off": "brak", + "on": "wykryto" + }, + "vibration": { + "off": "brak", + "on": "wykryto" + }, + "window": { + "off": "zamkni\u0119te", + "on": "otwarte" + } + }, + "title": "Sensor binarny" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/pt-BR.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/pt-BR.json new file mode 100644 index 00000000000..52671ca0425 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/pt-BR.json @@ -0,0 +1,85 @@ +{ + "state": { + "_": { + "off": "Desligado", + "on": "Ligado" + }, + "battery": { + "off": "Normal", + "on": "Fraca" + }, + "cold": { + "off": "Normal", + "on": "Frio" + }, + "connectivity": { + "off": "Desconectado", + "on": "Conectado" + }, + "door": { + "off": "Fechado", + "on": "Aberto" + }, + "garage_door": { + "off": "Fechado", + "on": "Aberto" + }, + "gas": { + "off": "Limpo", + "on": "Detectado" + }, + "heat": { + "off": "Normal", + "on": "Quente" + }, + "lock": { + "off": "Trancado", + "on": "Desbloqueado" + }, + "moisture": { + "off": "Seco", + "on": "Molhado" + }, + "motion": { + "off": "Desligado", + "on": "Detectado" + }, + "occupancy": { + "off": "Desocupado", + "on": "Detectado" + }, + "opening": { + "off": "Fechado", + "on": "Aberto" + }, + "presence": { + "off": "Ausente", + "on": "Em casa" + }, + "problem": { + "off": "OK", + "on": "Problema" + }, + "safety": { + "off": "Seguro", + "on": "N\u00e3o seguro" + }, + "smoke": { + "off": "Limpo", + "on": "Detectado" + }, + "sound": { + "off": "Limpo", + "on": "Detectado" + }, + "vibration": { + "off": "Limpo", + "on": "Detectado" + }, + "window": { + "off": "Fechado", + "on": "Aberto" + } + }, + "title": "Sensor bin\u00e1rio" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/pt.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/pt.json new file mode 100644 index 00000000000..9d7fdda1006 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/pt.json @@ -0,0 +1,191 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "a bateria {entity_name} est\u00e1 baixa", + "is_cold": "{entity_name} est\u00e1 frio", + "is_connected": "{entity_name} est\u00e1 ligado", + "is_gas": "{entity_name} est\u00e1 a detectar g\u00e1s", + "is_hot": "{entity_name} est\u00e1 quente", + "is_light": "{entity_name} est\u00e1 a detectar luz", + "is_locked": "{entity_name} est\u00e1 fechado", + "is_moist": "{entity_name} est\u00e1 h\u00famido", + "is_motion": "{entity_name} est\u00e1 a detectar movimento", + "is_moving": "{entity_name} est\u00e1 a mexer", + "is_no_gas": "{entity_name} n\u00e3o est\u00e1 a detectar g\u00e1s", + "is_no_light": "{entity_name} n\u00e3o est\u00e1 a detectar a luz", + "is_no_motion": "{entity_name} n\u00e3o est\u00e1 a detectar movimento", + "is_no_problem": "{entity_name} n\u00e3o est\u00e1 a detectar o problema", + "is_no_smoke": "{entity_name} n\u00e3o est\u00e1 a detectar fumo", + "is_no_sound": "{entity_name} n\u00e3o est\u00e1 a detectar som", + "is_no_vibration": "{entity_name} n\u00e3o est\u00e1 a detectar vibra\u00e7\u00f5es", + "is_not_bat_low": "{entity_name} a bateria est\u00e1 normal", + "is_not_cold": "{entity_name} n\u00e3o est\u00e1 frio", + "is_not_connected": "{entity_name} est\u00e1 desligado", + "is_not_hot": "{entity_name} n\u00e3o est\u00e1 quente", + "is_not_locked": "{entity_name} est\u00e1 destrancado", + "is_not_moist": "{entity_name} est\u00e1 seco", + "is_not_moving": "{entity_name} n\u00e3o est\u00e1 a mexer", + "is_not_occupied": "{entity_name} n\u00e3o est\u00e1 ocupado", + "is_not_open": "{entity_name} est\u00e1 fechada", + "is_not_plugged_in": "{entity_name} est\u00e1 desconectado", + "is_not_powered": "{entity_name} n\u00e3o est\u00e1 alimentado", + "is_not_present": "{entity_name} n\u00e3o est\u00e1 presente", + "is_not_unsafe": "{entity_name} est\u00e1 seguro", + "is_occupied": "{entity_name} est\u00e1 ocupado", + "is_off": "{entity_name} est\u00e1 desligado", + "is_on": "{entity_name} est\u00e1 ligado", + "is_open": "{entity_name} est\u00e1 aberto", + "is_plugged_in": "{entity_name} est\u00e1 conectado", + "is_powered": "{entity_name} est\u00e1 alimentado", + "is_present": "{entity_name} est\u00e1 presente", + "is_problem": "{entity_name} est\u00e1 a detectar um problema", + "is_smoke": "{entity_name} est\u00e1 a detectar fumo", + "is_sound": "{entity_name} est\u00e1 a detectar som", + "is_unsafe": "{entity_name} n\u00e3o \u00e9 seguro", + "is_vibration": "{entity_name} est\u00e1 a detectar vibra\u00e7\u00f5es" + }, + "trigger_type": { + "bat_low": "{entity_name} com bateria fraca", + "cold": "{entity_name} ficou frio", + "connected": "{entity_name} est\u00e1 ligado", + "gas": "{entity_name} detectou g\u00e1s", + "hot": "{entity_name} ficou quente", + "light": "{entity_name} detectou luz", + "locked": "{entity_name} fechou", + "moist": "ficou h\u00famido {entity_name}", + "motion": "{entity_name} detectou movimento", + "moving": "{entity_name} come\u00e7ou a mover-se", + "no_gas": "{entity_name} deixou de detectar g\u00e1s", + "no_light": "{entity_name} deixou de detectar luz", + "no_motion": "{entity_name} deixou de detectar movimento", + "no_problem": "{entity_name} deixou de detectar problemas", + "no_smoke": "{entity_name} deixou de detectar fumo", + "no_sound": "{entity_name} deixou de detectar som", + "no_vibration": "{entity_name} deixou de detectar vibra\u00e7\u00e3o", + "not_bat_low": "{entity_name} bateria normal", + "not_cold": "{entity_name} deixou de estar frio", + "not_connected": "{entity_name} est\u00e1 desligado", + "not_hot": "{entity_name} deixou de estar quente", + "not_locked": "{entity_name} destrancado", + "not_moist": "{entity_name} ficou seco", + "not_moving": "{entity_name} deixou de se mover", + "not_occupied": "{entity_name} deixou de estar ocupado", + "not_opened": "fechado {entity_name}", + "not_plugged_in": "{entity_name} desligado", + "not_powered": "{entity_name} n\u00e3o alimentado", + "not_present": "ausente {entity_name}", + "not_unsafe": "ficou seguro {entity_name}", + "occupied": "ficou ocupado {entity_name}", + "opened": "{entity_name} aberto", + "plugged_in": "{entity_name} ligado", + "powered": "{entity_name} alimentado", + "present": "{entity_name} presente", + "problem": "foi detectado problema em {entity_name}", + "smoke": "foi detectado fumo em {entity_name}", + "sound": "foram detectadas sons em {entity_name}", + "turned_off": "foi desligado {entity_name}", + "turned_on": "foi ligado {entity_name}", + "unsafe": "ficou inseguro {entity_name}", + "vibration": "foram detectadas vibra\u00e7\u00f5es em {entity_name}" + } + }, + "state": { + "_": { + "off": "Desligado", + "on": "Ligado" + }, + "battery": { + "off": "Normal", + "on": "Baixo" + }, + "battery_charging": { + "off": "Sem carregar", + "on": "A carregar" + }, + "cold": { + "off": "Normal", + "on": "Frio" + }, + "connectivity": { + "off": "Desligado", + "on": "Ligado" + }, + "door": { + "off": "Fechada", + "on": "Aberta" + }, + "garage_door": { + "off": "Fechada", + "on": "Aberta" + }, + "gas": { + "off": "Limpo", + "on": "Detectado" + }, + "heat": { + "off": "Normal", + "on": "Quente" + }, + "light": { + "off": "Sem luz", + "on": "Com luz" + }, + "lock": { + "off": "Trancada", + "on": "Destrancada" + }, + "moisture": { + "off": "Seco", + "on": "H\u00famido" + }, + "motion": { + "off": "Limpo", + "on": "Detectado" + }, + "moving": { + "off": "Parado", + "on": "Em movimento" + }, + "occupancy": { + "off": "Limpo", + "on": "Detectado" + }, + "opening": { + "off": "Fechado", + "on": "Aberto" + }, + "plug": { + "off": "Desligado", + "on": "Ligado" + }, + "presence": { + "off": "Fora", + "on": "Casa" + }, + "problem": { + "off": "OK", + "on": "Problema" + }, + "safety": { + "off": "Seguro", + "on": "Inseguro" + }, + "smoke": { + "off": "Limpo", + "on": "Detectado" + }, + "sound": { + "off": "Limpo", + "on": "Detectado" + }, + "vibration": { + "off": "Limpo", + "on": "Detetado" + }, + "window": { + "off": "Fechada", + "on": "Aberta" + } + }, + "title": "Sensor bin\u00e1rio" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ro.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ro.json new file mode 100644 index 00000000000..4ad892d234a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ro.json @@ -0,0 +1,128 @@ +{ + "device_automation": { + "condition_type": { + "is_off": "{entity_name} oprit", + "is_on": "{entity_name} pornit" + }, + "trigger_type": { + "gas": "{entity_name} a \u00eenceput s\u0103 detecteze gaz", + "hot": "{entity_name} a devenit fierbinte", + "locked": "{entity_name} blocat", + "motion": "{entity_name} a \u00eenceput s\u0103 detecteze mi\u0219care", + "moving": "{entity_name} a \u00eenceput s\u0103 se mi\u0219te", + "no_light": "{entity_name} a oprit detectarea luminii", + "no_motion": "{entity_name} a oprit detectarea mi\u0219c\u0103rii", + "no_problem": "{entity_name} a oprit detectarea problemei", + "no_smoke": "{entity_name} a oprit detectarea fumului", + "no_sound": "{entity_name} a oprit detectarea de sunet", + "no_vibration": "{entity_name} a oprit detectarea vibra\u021biilor", + "not_bat_low": "{entity_name} baterie normal\u0103", + "not_cold": "{entity_name} nu mai este rece", + "not_connected": "{entity_name} deconectat", + "not_hot": "{entity_name} nu mai este fierbinte", + "not_locked": "{entity_name} deblocat", + "not_moist": "{entity_name} a devenit uscat", + "not_moving": "{entity_name} a \u00eencetat mi\u0219carea", + "not_occupied": "{entity_name} a devenit neocupat", + "not_plugged_in": "{entity_name} deconectat", + "not_powered": "{entity_name} nu este alimentat", + "not_present": "{entity_name} nu este prezent", + "not_unsafe": "{entity_name} a devenit sigur", + "occupied": "{entity_name} a devenit ocupat", + "opened": "{entity_name} deschis", + "plugged_in": "{entity_name} conectat", + "powered": "{entity_name} alimentat", + "present": "{entity_name} prezent", + "problem": "{entity_name} a \u00eenceput detectarea unei probleme", + "smoke": "{entity_name} a \u00eenceput s\u0103 detecteze fum", + "sound": "{entity_name} a \u00eenceput s\u0103 detecteze sunetul", + "turned_off": "{entity_name} oprit", + "turned_on": "{entity_name} pornit", + "unsafe": "{entity_name} a devenit nesigur", + "vibration": "{entity_name} a \u00eenceput s\u0103 detecteze vibra\u021biile" + } + }, + "state": { + "_": { + "off": "Oprit", + "on": "Pornit" + }, + "battery": { + "off": "Normal", + "on": "Sc\u0103zuta" + }, + "cold": { + "off": "Normal", + "on": "Rece" + }, + "connectivity": { + "off": "Deconectat", + "on": "Conectat" + }, + "door": { + "off": "\u00cenchis", + "on": "Deschis" + }, + "garage_door": { + "off": "\u00cenchis", + "on": "Deschis" + }, + "gas": { + "off": "Liber", + "on": "Detec\u021bie" + }, + "heat": { + "off": "Normal", + "on": "Fierbinte" + }, + "lock": { + "off": "Blocat", + "on": "Deblocat" + }, + "moisture": { + "off": "Uscat", + "on": "Umed" + }, + "motion": { + "off": "Liber", + "on": "Detec\u021bie" + }, + "occupancy": { + "off": "Liber", + "on": "Detec\u021bie" + }, + "opening": { + "off": "\u00cenchis", + "on": "Deschis" + }, + "presence": { + "off": "Plecat", + "on": "Acas\u0103" + }, + "problem": { + "off": "OK", + "on": "Problem\u0103" + }, + "safety": { + "off": "Sigur", + "on": "Nesigur" + }, + "smoke": { + "off": "Liber", + "on": "Detec\u021bie" + }, + "sound": { + "off": "Liber", + "on": "Detec\u021bie" + }, + "vibration": { + "off": "Liber", + "on": "Detec\u021bie" + }, + "window": { + "off": "\u00cenchis", + "on": "Deschis" + } + }, + "title": "Senzor binar" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ru.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ru.json new file mode 100644 index 00000000000..2db1506b392 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ru.json @@ -0,0 +1,191 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} \u0432 \u0440\u0430\u0437\u0440\u044f\u0436\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_cold": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043e\u0445\u043b\u0430\u0436\u0434\u0435\u043d\u0438\u0435", + "is_connected": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "is_gas": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0433\u0430\u0437", + "is_hot": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0435\u0432", + "is_light": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u0432\u0435\u0442", + "is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_moist": "{entity_name} \u0432 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438 \"\u0412\u043b\u0430\u0436\u043d\u043e\"", + "is_motion": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "is_moving": "{entity_name} \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0430\u0435\u0442\u0441\u044f", + "is_no_gas": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0433\u0430\u0437", + "is_no_light": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u0432\u0435\u0442", + "is_no_motion": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "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_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", + "is_not_connected": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "is_not_hot": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0435\u0432", + "is_not_locked": "{entity_name} \u0432 \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_not_moist": "{entity_name} \u0432 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438 \"\u0421\u0443\u0445\u043e\"", + "is_not_moving": "{entity_name} \u043d\u0435 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0430\u0435\u0442\u0441\u044f", + "is_not_occupied": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "is_not_open": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_not_plugged_in": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "is_not_powered": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0438\u0442\u0430\u043d\u0438\u0435", + "is_not_present": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "is_not_unsafe": "{entity_name} \u0432 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_occupied": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_open": "{entity_name} \u0432 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_plugged_in": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "is_powered": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0438\u0442\u0430\u043d\u0438\u0435", + "is_present": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "is_problem": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "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_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": { + "bat_low": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0438\u0437\u043a\u0438\u0439 \u0437\u0430\u0440\u044f\u0434", + "cold": "{entity_name} \u043e\u0445\u043b\u0430\u0436\u0434\u0430\u0435\u0442\u0441\u044f", + "connected": "{entity_name} \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "gas": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0433\u0430\u0437", + "hot": "{entity_name} \u043d\u0430\u0433\u0440\u0435\u0432\u0430\u0435\u0442\u0441\u044f", + "light": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0441\u0432\u0435\u0442", + "locked": "{entity_name} \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f", + "moist": "{entity_name} \u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f \u0432\u043b\u0430\u0436\u043d\u044b\u043c", + "motion": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "moving": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u0435", + "no_gas": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0433\u0430\u0437", + "no_light": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0441\u0432\u0435\u0442", + "no_motion": "{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\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "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_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", + "not_connected": "{entity_name} \u043e\u0442\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "not_hot": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0435\u0432\u0430\u0442\u044c\u0441\u044f", + "not_locked": "{entity_name} \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f", + "not_moist": "{entity_name} \u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f \u0441\u0443\u0445\u0438\u043c", + "not_moving": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u0435", + "not_occupied": "{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\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "not_opened": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "not_plugged_in": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "not_powered": "{entity_name} \u043d\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u043f\u0438\u0442\u0430\u043d\u0438\u044f", + "not_present": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "not_unsafe": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c", + "occupied": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "opened": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "plugged_in": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "powered": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u043f\u0438\u0442\u0430\u043d\u0438\u044f", + "present": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "problem": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "smoke": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u044b\u043c", + "sound": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0437\u0432\u0443\u043a", + "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", + "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" + } + }, + "state": { + "_": { + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + }, + "battery": { + "off": "\u041d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u044b\u0439", + "on": "\u041d\u0438\u0437\u043a\u0438\u0439" + }, + "battery_charging": { + "off": "\u041d\u0435 \u0437\u0430\u0440\u044f\u0436\u0430\u0435\u0442\u0441\u044f", + "on": "\u0417\u0430\u0440\u044f\u0436\u0430\u0435\u0442\u0441\u044f" + }, + "cold": { + "off": "\u041d\u043e\u0440\u043c\u0430", + "on": "\u041e\u0445\u043b\u0430\u0436\u0434\u0435\u043d\u0438\u0435" + }, + "connectivity": { + "off": "\u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + }, + "door": { + "off": "\u0417\u0430\u043a\u0440\u044b\u0442\u0430", + "on": "\u041e\u0442\u043a\u0440\u044b\u0442\u0430" + }, + "garage_door": { + "off": "\u0417\u0430\u043a\u0440\u044b\u0442\u044b", + "on": "\u041e\u0442\u043a\u0440\u044b\u0442\u044b" + }, + "gas": { + "off": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d", + "on": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d" + }, + "heat": { + "off": "\u041d\u043e\u0440\u043c\u0430", + "on": "\u041d\u0430\u0433\u0440\u0435\u0432" + }, + "light": { + "off": "\u041d\u0435\u0442 \u0441\u0432\u0435\u0442\u0430", + "on": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u0441\u0432\u0435\u0442" + }, + "lock": { + "off": "\u0417\u0430\u043a\u0440\u044b\u0442", + "on": "\u041e\u0442\u043a\u0440\u044b\u0442" + }, + "moisture": { + "off": "\u0421\u0443\u0445\u043e", + "on": "\u0412\u043b\u0430\u0436\u043d\u043e" + }, + "motion": { + "off": "\u041d\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f ", + "on": "\u0414\u0432\u0438\u0436\u0435\u043d\u0438\u0435" + }, + "moving": { + "off": "\u041d\u0435 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0430\u0435\u0442\u0441\u044f", + "on": "\u041f\u0435\u0440\u0435\u043c\u0435\u0449\u0430\u0435\u0442\u0441\u044f" + }, + "occupancy": { + "off": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e", + "on": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e" + }, + "opening": { + "off": "\u0417\u0430\u043a\u0440\u044b\u0442\u043e", + "on": "\u041e\u0442\u043a\u0440\u044b\u0442\u043e" + }, + "plug": { + "off": "\u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + }, + "presence": { + "off": "\u041d\u0435 \u0434\u043e\u043c\u0430", + "on": "\u0414\u043e\u043c\u0430" + }, + "problem": { + "off": "\u041e\u041a", + "on": "\u041f\u0440\u043e\u0431\u043b\u0435\u043c\u0430" + }, + "safety": { + "off": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e", + "on": "\u041d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e" + }, + "smoke": { + "off": "\u041d\u0435\u0442 \u0434\u044b\u043c\u0430", + "on": "\u0414\u044b\u043c" + }, + "sound": { + "off": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d", + "on": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d" + }, + "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" + }, + "window": { + "off": "\u0417\u0430\u043a\u0440\u044b\u0442\u043e", + "on": "\u041e\u0442\u043a\u0440\u044b\u0442\u043e" + } + }, + "title": "\u0411\u0438\u043d\u0430\u0440\u043d\u044b\u0439 \u0441\u0435\u043d\u0441\u043e\u0440" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/sk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/sk.json new file mode 100644 index 00000000000..5cff82615ae --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/sk.json @@ -0,0 +1,85 @@ +{ + "state": { + "_": { + "off": "Neakt\u00edvny", + "on": "Akt\u00edvny" + }, + "battery": { + "off": "Norm\u00e1lna", + "on": "Slab\u00e1" + }, + "cold": { + "off": "Norm\u00e1lny", + "on": "Studen\u00fd" + }, + "connectivity": { + "off": "Odpojen\u00fd", + "on": "Pripojen\u00fd" + }, + "door": { + "off": "Zatvoren\u00e9", + "on": "Otvoren\u00e9" + }, + "garage_door": { + "off": "Zatvoren\u00e9", + "on": "Otvoren\u00e9" + }, + "gas": { + "off": "\u017diadny plyn", + "on": "Zachyten\u00fd plyn" + }, + "heat": { + "off": "Norm\u00e1lny", + "on": "Hor\u00faci" + }, + "lock": { + "off": "Zamknut\u00fd", + "on": "Odomknut\u00fd" + }, + "moisture": { + "off": "Sucho", + "on": "Vlhko" + }, + "motion": { + "off": "K\u013eud", + "on": "Pohyb" + }, + "occupancy": { + "off": "Vo\u013en\u00e9", + "on": "Obsaden\u00e9" + }, + "opening": { + "off": "Zatvoren\u00e9", + "on": "Otvoren\u00e9" + }, + "presence": { + "off": "Pre\u010d", + "on": "Doma" + }, + "problem": { + "off": "OK", + "on": "Probl\u00e9m" + }, + "safety": { + "off": "Zabezpe\u010den\u00e9", + "on": "Nezabezpe\u010den\u00e9" + }, + "smoke": { + "off": "\u017diadny dym", + "on": "Zachyten\u00fd dym" + }, + "sound": { + "off": "Ticho", + "on": "Zachyten\u00fd zvuk" + }, + "vibration": { + "off": "K\u013eud", + "on": "Zachyten\u00e9 vibr\u00e1cie" + }, + "window": { + "off": "Zatvoren\u00e9", + "on": "Otvoren\u00e9" + } + }, + "title": "Bin\u00e1rny senzor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/sl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/sl.json new file mode 100644 index 00000000000..02c4eedeba9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/sl.json @@ -0,0 +1,189 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} ima prazno baterijo", + "is_cold": "{entity_name} je hladen", + "is_connected": "{entity_name} je povezan", + "is_gas": "{entity_name} zaznava plin", + "is_hot": "{entity_name} je vro\u010d", + "is_light": "{entity_name} zaznava svetlobo", + "is_locked": "{entity_name} je zaklenjen", + "is_moist": "{entity_name} je vla\u017een", + "is_motion": "{entity_name} zaznava gibanje", + "is_moving": "{entity_name} se premika", + "is_no_gas": "{entity_name} ne zaznava plina", + "is_no_light": "{entity_name} ne zaznava svetlobe", + "is_no_motion": "{entity_name} ne zaznava gibanja", + "is_no_problem": "{entity_name} ne zaznava te\u017eav", + "is_no_smoke": "{entity_name} ne zaznava dima", + "is_no_sound": "{entity_name} ne zaznava zvoka", + "is_no_vibration": "{entity_name} ne zazna vibracij", + "is_not_bat_low": "{entity_name} baterija je polna", + "is_not_cold": "{entity_name} ni hladen", + "is_not_connected": "{entity_name} ni povezan", + "is_not_hot": "{entity_name} ni vro\u010d", + "is_not_locked": "{entity_name} je odklenjen", + "is_not_moist": "{entity_name} je suh", + "is_not_moving": "{entity_name} se ne premika", + "is_not_occupied": "{entity_name} ni zaseden", + "is_not_open": "{entity_name} je zaprt", + "is_not_plugged_in": "{entity_name} je odklopljen", + "is_not_powered": "{entity_name} ni napajan", + "is_not_present": "{entity_name} ni prisoten", + "is_not_unsafe": "{entity_name} je varen", + "is_occupied": "{entity_name} je zaseden", + "is_off": "{entity_name} je izklopljen", + "is_on": "{entity_name} je vklopljen", + "is_open": "{entity_name} je odprt", + "is_plugged_in": "{entity_name} je priklju\u010den", + "is_powered": "{entity_name} je vklopljen", + "is_present": "{entity_name} je prisoten", + "is_problem": "{entity_name} zaznava te\u017eavo", + "is_smoke": "{entity_name} zaznava dim", + "is_sound": "{entity_name} zaznava zvok", + "is_unsafe": "{entity_name} ni varen", + "is_vibration": "{entity_name} zaznava vibracije" + }, + "trigger_type": { + "bat_low": "{entity_name} ima prazno baterijo", + "cold": "{entity_name} je postal hladen", + "connected": "{entity_name} povezan", + "gas": "{entity_name} za\u010del zaznavati plin", + "hot": "{entity_name} je postal vro\u010d", + "light": "{entity_name} za\u010del zaznavati svetlobo", + "locked": "{entity_name} zaklenjen", + "moist": "{entity_name} postal vla\u017een", + "motion": "{entity_name} za\u010del zaznavati gibanje", + "moving": "{entity_name} se je za\u010del premikati", + "no_gas": "{entity_name} prenehal zaznavati plin", + "no_light": "{entity_name} prenehal zaznavati svetlobo", + "no_motion": "{entity_name} prenehal zaznavati gibanje", + "no_problem": "{entity_name} prenehal odkrivati te\u017eavo", + "no_smoke": "{entity_name} prenehal zaznavati dim", + "no_sound": "{entity_name} prenehal zaznavati zvok", + "no_vibration": "{entity_name} prenehal zaznavati vibracije", + "not_bat_low": "{entity_name} ima polno baterijo", + "not_cold": "{entity_name} ni ve\u010d hladen", + "not_connected": "{entity_name} prekinjen", + "not_hot": "{entity_name} ni ve\u010d vro\u010d", + "not_locked": "{entity_name} odklenjen", + "not_moist": "{entity_name} je postalo suh", + "not_moving": "{entity_name} se je prenehal premikati", + "not_occupied": "{entity_name} ni zaseden", + "not_opened": "{entity_name} zaprto", + "not_plugged_in": "{entity_name} odklopljen", + "not_powered": "{entity_name} ni napajan", + "not_present": "{entity_name} ni prisoten", + "not_unsafe": "{entity_name} je postal varen", + "occupied": "{entity_name} postal zaseden", + "opened": "{entity_name} odprl", + "plugged_in": "{entity_name} priklju\u010den", + "powered": "{entity_name} priklopljen", + "present": "{entity_name} prisoten", + "problem": "{entity_name} za\u010del odkrivati te\u017eavo", + "smoke": "{entity_name} za\u010del zaznavati dim", + "sound": "{entity_name} za\u010del zaznavati zvok", + "turned_off": "{entity_name} izklopljen", + "turned_on": "{entity_name} vklopljen", + "unsafe": "{entity_name} je postal nevaren", + "vibration": "{entity_name} je za\u010del odkrivat vibracije" + } + }, + "state": { + "_": { + "off": "Izklju\u010den", + "on": "Vklopljen" + }, + "battery": { + "off": "Normalno", + "on": "Nizko" + }, + "battery_charging": { + "off": "Se ne polni" + }, + "cold": { + "off": "Normalno", + "on": "Hladno" + }, + "connectivity": { + "off": "Povezava prekinjena", + "on": "Povezan" + }, + "door": { + "off": "Zaprto", + "on": "Odprto" + }, + "garage_door": { + "off": "Zaprto", + "on": "Odprto" + }, + "gas": { + "off": "\u010cisto", + "on": "Zaznano" + }, + "heat": { + "off": "Normalno", + "on": "Vro\u010de" + }, + "light": { + "off": "Ni lu\u010di", + "on": "Zaznana svetloba" + }, + "lock": { + "off": "Zaklenjeno", + "on": "Odklenjeno" + }, + "moisture": { + "off": "Suho", + "on": "Mokro" + }, + "motion": { + "off": "\u010cisto", + "on": "Zaznano" + }, + "moving": { + "on": "Premikanje" + }, + "occupancy": { + "off": "\u010cisto", + "on": "Zaznano" + }, + "opening": { + "off": "Zaprto", + "on": "Odprto" + }, + "plug": { + "off": "Odklopljeno", + "on": "Priklopljeno" + }, + "presence": { + "off": "Odsoten", + "on": "Doma" + }, + "problem": { + "off": "OK", + "on": "Te\u017eava" + }, + "safety": { + "off": "Varno", + "on": "Nevarno" + }, + "smoke": { + "off": "\u010cisto", + "on": "Zaznano" + }, + "sound": { + "off": "\u010cisto", + "on": "Zaznano" + }, + "vibration": { + "off": "\u010cisto", + "on": "Zaznano" + }, + "window": { + "off": "Zaprto", + "on": "Odprto" + } + }, + "title": "Binarni senzor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/sv.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/sv.json new file mode 100644 index 00000000000..c651d895fda --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/sv.json @@ -0,0 +1,175 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name}-batteriet \u00e4r l\u00e5gt", + "is_cold": "{entity_name} \u00e4r kall", + "is_connected": "{entity_name} \u00e4r ansluten", + "is_gas": "{entity_name} detekterar gas", + "is_hot": "{entity_name} \u00e4r varm", + "is_light": "{entity_name} uppt\u00e4cker ljus", + "is_locked": "{entity_name} \u00e4r l\u00e5st", + "is_moist": "{entity_name} \u00e4r fuktig", + "is_motion": "{entity_name} detekterar r\u00f6relse", + "is_moving": "{entity_name} r\u00f6r sig", + "is_no_gas": "{entity_name} uppt\u00e4cker inte gas", + "is_no_light": "{entity_name} uppt\u00e4cker inte ljus", + "is_no_motion": "{entity_name} detekterar inte r\u00f6relse", + "is_no_problem": "{entity_name} uppt\u00e4cker inte problem", + "is_no_smoke": "{entity_name} detekterar inte r\u00f6k", + "is_no_sound": "{entity_name} uppt\u00e4cker inte ljud", + "is_no_vibration": "{entity_name} uppt\u00e4cker inte vibrationer", + "is_not_bat_low": "{entity_name} batteri \u00e4r normalt", + "is_not_cold": "{entity_name} \u00e4r inte kall", + "is_not_connected": "{entity_name} \u00e4r fr\u00e5nkopplad", + "is_not_hot": "{entity_name} \u00e4r inte varm", + "is_not_locked": "{entity_name} \u00e4r ol\u00e5st", + "is_not_moist": "{entity_name} \u00e4r torr", + "is_not_moving": "{entity_name} r\u00f6r sig inte", + "is_not_occupied": "{entity_name} \u00e4r inte upptagen", + "is_not_open": "{entity_name} \u00e4r st\u00e4ngd", + "is_not_plugged_in": "{entity_name} \u00e4r urkopplad", + "is_not_powered": "{entity_name} \u00e4r inte str\u00f6mf\u00f6rd", + "is_not_present": "{entity_name} finns inte", + "is_not_unsafe": "{entity_name} \u00e4r s\u00e4ker", + "is_occupied": "{entity_name} \u00e4r upptagen", + "is_off": "{entity_name} \u00e4r avst\u00e4ngd", + "is_on": "{entity_name} \u00e4r p\u00e5", + "is_open": "{entity_name} \u00e4r \u00f6ppen", + "is_plugged_in": "{entity_name} \u00e4r ansluten", + "is_powered": "{entity_name} \u00e4r str\u00f6mf\u00f6rd", + "is_present": "{entity_name} \u00e4r n\u00e4rvarande", + "is_problem": "{entity_name} uppt\u00e4cker problem", + "is_smoke": "{entity_name} detekterar r\u00f6k", + "is_sound": "{entity_name} uppt\u00e4cker ljud", + "is_unsafe": "{entity_name} \u00e4r os\u00e4ker", + "is_vibration": "{entity_name} uppt\u00e4cker vibrationer" + }, + "trigger_type": { + "bat_low": "{entity_name} batteri l\u00e5gt", + "cold": "{entity_name} blev kall", + "connected": "{entity_name} ansluten", + "gas": "{entity_name} b\u00f6rjade detektera gas", + "hot": "{entity_name} blev varm", + "light": "{entity_name} b\u00f6rjade uppt\u00e4cka ljus", + "locked": "{entity_name} l\u00e5st", + "moist": "{entity_name} blev fuktig", + "motion": "{entity_name} b\u00f6rjade detektera r\u00f6relse", + "moving": "{entity_name} b\u00f6rjade r\u00f6ra sig", + "no_gas": "{entity_name} slutade uppt\u00e4cka gas", + "no_light": "{entity_name} slutade uppt\u00e4cka ljus", + "no_motion": "{entity_name} slutade uppt\u00e4cka r\u00f6relse", + "no_problem": "{entity_name} slutade uppt\u00e4cka problem", + "no_smoke": "{entity_name} slutade detektera r\u00f6k", + "no_sound": "{entity_name} slutade uppt\u00e4cka ljud", + "no_vibration": "{entity_name} slutade uppt\u00e4cka vibrationer", + "not_bat_low": "{entity_name} batteri normalt", + "not_cold": "{entity_name} blev inte kall", + "not_connected": "{entity_name} fr\u00e5nkopplad", + "not_hot": "{entity_name} blev inte varm", + "not_locked": "{entity_name} ol\u00e5st", + "not_moist": "{entity_name} blev torr", + "not_moving": "{entity_name} slutade r\u00f6ra sig", + "not_occupied": "{entity_name} blev inte upptagen", + "not_opened": "{entity_name} st\u00e4ngd", + "not_plugged_in": "{entity_name} urkopplad", + "not_powered": "{entity_name} inte str\u00f6mf\u00f6rd", + "not_present": "{entity_name} inte n\u00e4rvarande", + "not_unsafe": "{entity_name} blev s\u00e4ker", + "occupied": "{entity_name} blev upptagen", + "opened": "{entity_name} \u00f6ppnades", + "plugged_in": "{entity_name} ansluten", + "powered": "{entity_name} str\u00f6mf\u00f6rd", + "present": "{entity_name} n\u00e4rvarande", + "problem": "{entity_name} b\u00f6rjade uppt\u00e4cka problem", + "smoke": "{entity_name} b\u00f6rjade detektera r\u00f6k", + "sound": "{entity_name} b\u00f6rjade uppt\u00e4cka ljud", + "turned_off": "{entity_name} st\u00e4ngdes av", + "turned_on": "{entity_name} slogs p\u00e5", + "unsafe": "{entity_name} blev os\u00e4ker", + "vibration": "{entity_name} b\u00f6rjade detektera vibrationer" + } + }, + "state": { + "_": { + "off": "Av", + "on": "P\u00e5" + }, + "battery": { + "off": "Normal", + "on": "L\u00e5g" + }, + "cold": { + "off": "Normal", + "on": "Kallt" + }, + "connectivity": { + "off": "Fr\u00e5nkopplad", + "on": "Ansluten" + }, + "door": { + "off": "St\u00e4ngd", + "on": "\u00d6ppen" + }, + "garage_door": { + "off": "St\u00e4ngd", + "on": "\u00d6ppen" + }, + "gas": { + "off": "Klart", + "on": "Detekterad" + }, + "heat": { + "off": "Normal", + "on": "Varmt" + }, + "lock": { + "off": "L\u00e5st", + "on": "Ol\u00e5st" + }, + "moisture": { + "off": "Torr", + "on": "Bl\u00f6t" + }, + "motion": { + "off": "Klart", + "on": "Detekterad" + }, + "occupancy": { + "off": "Tomt", + "on": "Detekterad" + }, + "opening": { + "off": "St\u00e4ngd", + "on": "\u00d6ppen" + }, + "presence": { + "off": "Borta", + "on": "Hemma" + }, + "problem": { + "off": "Ok", + "on": "Problem" + }, + "safety": { + "off": "S\u00e4ker", + "on": "Os\u00e4ker" + }, + "smoke": { + "off": "Klart", + "on": "Detekterad" + }, + "sound": { + "off": "Klart", + "on": "Detekterad" + }, + "vibration": { + "off": "Klart", + "on": "Detekterad" + }, + "window": { + "off": "St\u00e4ngt", + "on": "\u00d6ppet" + } + }, + "title": "Bin\u00e4r sensor" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ta.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ta.json new file mode 100644 index 00000000000..a720b61c69c --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/ta.json @@ -0,0 +1,60 @@ +{ + "state": { + "_": { + "off": "\u0b86\u0b83\u0baa\u0bcd", + "on": "\u0b86\u0ba9\u0bcd " + }, + "gas": { + "off": "\u0ba4\u0bc6\u0bb3\u0bbf\u0bb5\u0bc1", + "on": "\u0b95\u0ba3\u0bcd\u0b9f\u0bb1\u0bbf\u0baf\u0baa\u0bcd\u0baa\u0b9f\u0bcd\u0b9f\u0ba4\u0bc1" + }, + "heat": { + "off": "\u0b86\u0b83\u0baa\u0bcd", + "on": "\u0b9a\u0bc2\u0b9f\u0bbe\u0ba9" + }, + "moisture": { + "off": "\u0b89\u0bb2\u0bb0\u0bcd", + "on": "\u0b88\u0bb0\u0bae\u0bcd" + }, + "motion": { + "off": "\u0ba4\u0bc6\u0bb3\u0bbf\u0bb5\u0bc1 ", + "on": "\u0b95\u0ba3\u0bcd\u0b9f\u0bb1\u0bbf\u0baf\u0baa\u0bcd\u0baa\u0b9f\u0bcd\u0b9f\u0ba4\u0bc1" + }, + "occupancy": { + "off": "\u0ba4\u0bc6\u0bb3\u0bbf\u0bb5\u0bc1 ", + "on": "\u0b95\u0ba3\u0bcd\u0b9f\u0bb1\u0bbf\u0baf\u0baa\u0bcd\u0baa\u0b9f\u0bcd\u0b9f\u0ba4\u0bc1" + }, + "opening": { + "off": "\u0bae\u0bc2\u0b9f\u0baa\u0bcd\u0baa\u0b9f\u0bcd\u0b9f\u0ba4\u0bc1", + "on": "\u0ba4\u0bbf\u0bb1\u0b95\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0b9f\u0bcd\u0b9f\u0bc1\u0bb3\u0bcd\u0bb3\u0ba4\u0bc1" + }, + "presence": { + "off": "\u0ba4\u0bca\u0bb2\u0bc8\u0bb5\u0bbf\u0bb2\u0bcd", + "on": "\u0bae\u0bc1\u0b95\u0baa\u0bcd\u0baa\u0bc1" + }, + "problem": { + "off": "\u0b9a\u0bb0\u0bbf", + "on": "\u0b9a\u0bbf\u0b95\u0bcd\u0b95\u0bb2\u0bcd" + }, + "safety": { + "off": "\u0baa\u0bbe\u0ba4\u0bc1\u0b95\u0bbe\u0baa\u0bcd\u0baa\u0bbe\u0ba9", + "on": "\u0baa\u0bbe\u0ba4\u0bc1\u0b95\u0bbe\u0baa\u0bcd\u0baa\u0bb1\u0bcd\u0bb1" + }, + "smoke": { + "off": "\u0ba4\u0bc6\u0bb3\u0bbf\u0bb5\u0bc1 ", + "on": "\u0b95\u0ba3\u0bcd\u0b9f\u0bb1\u0bbf\u0baf\u0baa\u0bcd\u0baa\u0b9f\u0bcd\u0b9f\u0ba4\u0bc1" + }, + "sound": { + "off": "\u0ba4\u0bc6\u0bb3\u0bbf\u0bb5\u0bc1 ", + "on": "\u0b95\u0ba3\u0bcd\u0b9f\u0bb1\u0bbf\u0baf\u0baa\u0bcd\u0baa\u0b9f\u0bcd\u0b9f\u0ba4\u0bc1" + }, + "vibration": { + "off": "\u0ba4\u0bc6\u0bb3\u0bbf\u0bb5\u0bc1 ", + "on": "\u0b95\u0ba3\u0bcd\u0b9f\u0bb1\u0bbf\u0baf\u0baa\u0bcd\u0baa\u0b9f\u0bcd\u0b9f\u0ba4\u0bc1" + }, + "window": { + "off": "\u0bae\u0bc2\u0b9f\u0baa\u0bcd\u0baa\u0b9f\u0bcd\u0b9f\u0bc1\u0bb3\u0bcd\u0bb3\u0ba4\u0bc1", + "on": "\u0ba4\u0bbf\u0bb1\u0b95\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0b9f\u0bcd\u0b9f\u0bc1\u0bb3\u0bcd\u0bb3\u0ba4\u0bc1" + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/te.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/te.json new file mode 100644 index 00000000000..4d5817d7492 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/te.json @@ -0,0 +1,84 @@ +{ + "state": { + "_": { + "off": "\u0c06\u0c2b\u0c4d", + "on": "\u0c06\u0c28\u0c4d" + }, + "battery": { + "off": "\u0c38\u0c3e\u0c27\u0c3e\u0c30\u0c23", + "on": "\u0c24\u0c15\u0c4d\u0c15\u0c41\u0c35" + }, + "cold": { + "on": "\u0c1a\u0c32\u0c4d\u0c32\u0c28\u0c3f" + }, + "connectivity": { + "off": "\u0c21\u0c3f\u0c38\u0c4d\u0c15\u0c28\u0c46\u0c15\u0c4d\u0c1f\u0c4d", + "on": "\u0c15\u0c28\u0c46\u0c15\u0c4d\u0c1f\u0c4d" + }, + "door": { + "off": "\u0c2e\u0c42\u0c38\u0c41\u0c15\u0c41\u0c02\u0c26\u0c3f", + "on": "\u0c24\u0c46\u0c30\u0c3f\u0c1a\u0c3f\u0c35\u0c41\u0c02\u0c26\u0c3f" + }, + "garage_door": { + "off": "\u0c2e\u0c42\u0c38\u0c41\u0c15\u0c41\u0c02\u0c26\u0c3f", + "on": "\u0c24\u0c46\u0c30\u0c3f\u0c1a\u0c3f\u0c35\u0c41\u0c02\u0c26\u0c3f" + }, + "gas": { + "off": "\u0c17\u0c4d\u0c2f\u0c3e\u0c38\u0c4d \u0c06\u0c2b\u0c4d", + "on": "\u0c17\u0c4d\u0c2f\u0c3e\u0c38\u0c4d \u0c06\u0c28\u0c4d" + }, + "heat": { + "off": "\u0c38\u0c3e\u0c27\u0c3e\u0c30\u0c23", + "on": "\u0c35\u0c47\u0c21\u0c3f" + }, + "lock": { + "off": "\u0c32\u0c3e\u0c15\u0c4d \u0c1a\u0c47\u0c2f\u0c2c\u0c21\u0c3f\u0c02\u0c26\u0c3f", + "on": "\u0c32\u0c3e\u0c15\u0c4d \u0c1a\u0c47\u0c2f\u0c2c\u0c21\u0c32\u0c47\u0c26\u0c41" + }, + "moisture": { + "off": "\u0c2a\u0c4a\u0c21\u0c3f", + "on": "\u0c24\u0c21\u0c3f" + }, + "motion": { + "off": "\u0c15\u0c26\u0c32\u0c3f\u0c15 \u0c32\u0c47\u0c26\u0c41", + "on": "\u0c15\u0c26\u0c32\u0c3f\u0c15 \u0c35\u0c41\u0c02\u0c26\u0c3f" + }, + "occupancy": { + "off": "\u0c09\u0c28\u0c3f\u0c15\u0c3f\u0c21\u0c3f \u0c32\u0c47\u0c26\u0c41", + "on": "\u0c09\u0c28\u0c3f\u0c15\u0c3f\u0c21\u0c3f \u0c09\u0c02\u0c26\u0c3f" + }, + "opening": { + "off": "\u0c2e\u0c42\u0c38\u0c3f\u0c35\u0c41\u0c02\u0c26\u0c3f", + "on": "\u0c24\u0c46\u0c30\u0c41\u0c1a\u0c41\u0c15\u0c41\u0c02\u0c1f\u0c4b\u0c02\u0c26\u0c3f" + }, + "presence": { + "off": "\u0c2c\u0c2f\u0c1f", + "on": "\u0c07\u0c02\u0c1f" + }, + "problem": { + "off": "OK", + "on": "\u0c38\u0c2e\u0c38\u0c4d\u0c2f" + }, + "safety": { + "off": "\u0c15\u0c4d\u0c37\u0c47\u0c2e\u0c02", + "on": "\u0c15\u0c4d\u0c37\u0c47\u0c2e\u0c02 \u0c15\u0c3e\u0c26\u0c41" + }, + "smoke": { + "off": "\u0c2a\u0c4a\u0c17 \u0c32\u0c47\u0c26\u0c41", + "on": "\u0c2a\u0c4a\u0c17 \u0c35\u0c41\u0c02\u0c26\u0c3f" + }, + "sound": { + "off": "\u0c36\u0c2c\u0c4d\u0c27\u0c02 \u0c32\u0c47\u0c26\u0c41", + "on": "\u0c36\u0c2c\u0c4d\u0c27\u0c02 \u0c35\u0c41\u0c02\u0c26\u0c3f" + }, + "vibration": { + "off": "\u0c15\u0c26\u0c32\u0c1f\u0c4d\u0c32\u0c47\u0c26\u0c41", + "on": "\u0c15\u0c26\u0c41\u0c32\u0c41\u0c24\u0c4b\u0c02\u0c26\u0c3f" + }, + "window": { + "off": "\u0c2e\u0c42\u0c38\u0c41\u0c15\u0c41\u0c02\u0c26\u0c3f", + "on": "\u0c24\u0c46\u0c30\u0c3f\u0c1a\u0c3f\u0c35\u0c41\u0c02\u0c26\u0c3f" + } + }, + "title": "\u0c2c\u0c48\u0c28\u0c30\u0c40 \u0c38\u0c46\u0c28\u0c4d\u0c38\u0c3e\u0c30\u0c4d" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/th.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/th.json new file mode 100644 index 00000000000..b8f41eb2b73 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/th.json @@ -0,0 +1,85 @@ +{ + "state": { + "_": { + "off": "\u0e1b\u0e34\u0e14", + "on": "\u0e40\u0e1b\u0e34\u0e14" + }, + "battery": { + "off": "\u0e1b\u0e01\u0e15\u0e34", + "on": "\u0e15\u0e48\u0e33" + }, + "cold": { + "off": "\u0e1b\u0e01\u0e15\u0e34", + "on": "\u0e2b\u0e19\u0e32\u0e27" + }, + "connectivity": { + "off": "\u0e15\u0e31\u0e14\u0e01\u0e32\u0e23\u0e40\u0e0a\u0e37\u0e48\u0e2d\u0e21\u0e15\u0e48\u0e2d", + "on": "\u0e40\u0e0a\u0e37\u0e48\u0e2d\u0e21\u0e15\u0e48\u0e2d\u0e41\u0e25\u0e49\u0e27" + }, + "door": { + "off": "\u0e1b\u0e34\u0e14\u0e41\u0e25\u0e49\u0e27", + "on": "\u0e40\u0e1b\u0e34\u0e14" + }, + "garage_door": { + "off": "\u0e1b\u0e34\u0e14\u0e41\u0e25\u0e49\u0e27", + "on": "\u0e40\u0e1b\u0e34\u0e14" + }, + "gas": { + "off": "\u0e44\u0e21\u0e48\u0e1e\u0e1a\u0e41\u0e01\u0e4a\u0e2a", + "on": "\u0e15\u0e23\u0e27\u0e08\u0e1e\u0e1a\u0e41\u0e01\u0e4a\u0e2a" + }, + "heat": { + "off": "\u0e1b\u0e01\u0e15\u0e34", + "on": "\u0e23\u0e49\u0e2d\u0e19" + }, + "lock": { + "off": "\u0e25\u0e47\u0e2d\u0e04\u0e2d\u0e22\u0e39\u0e48", + "on": "\u0e1b\u0e25\u0e14\u0e25\u0e47\u0e2d\u0e04\u0e41\u0e25\u0e49\u0e27" + }, + "moisture": { + "off": "\u0e41\u0e2b\u0e49\u0e07", + "on": "\u0e40\u0e1b\u0e35\u0e22\u0e01" + }, + "motion": { + "off": "\u0e44\u0e21\u0e48\u0e1e\u0e1a\u0e01\u0e32\u0e23\u0e40\u0e04\u0e25\u0e37\u0e48\u0e2d\u0e19\u0e44\u0e2b\u0e27", + "on": "\u0e1e\u0e1a\u0e01\u0e32\u0e23\u0e40\u0e04\u0e25\u0e37\u0e48\u0e2d\u0e19\u0e44\u0e2b\u0e27" + }, + "occupancy": { + "off": "\u0e44\u0e21\u0e48\u0e1e\u0e1a", + "on": "\u0e1e\u0e1a" + }, + "opening": { + "off": "\u0e1b\u0e34\u0e14", + "on": "\u0e40\u0e1b\u0e34\u0e14" + }, + "presence": { + "off": "\u0e44\u0e21\u0e48\u0e2d\u0e22\u0e39\u0e48", + "on": "\u0e2d\u0e22\u0e39\u0e48\u0e1a\u0e49\u0e32\u0e19" + }, + "problem": { + "off": "\u0e15\u0e01\u0e25\u0e07", + "on": "\u0e1b\u0e31\u0e0d\u0e2b\u0e32" + }, + "safety": { + "off": "\u0e1b\u0e34\u0e14", + "on": "\u0e40\u0e1b\u0e34\u0e14" + }, + "smoke": { + "off": "\u0e44\u0e21\u0e48\u0e1e\u0e1a\u0e04\u0e27\u0e31\u0e19", + "on": "\u0e1e\u0e1a\u0e04\u0e27\u0e31\u0e19" + }, + "sound": { + "off": "\u0e44\u0e21\u0e48\u0e44\u0e14\u0e49\u0e22\u0e34\u0e19", + "on": "\u0e44\u0e14\u0e49\u0e22\u0e34\u0e19" + }, + "vibration": { + "off": "\u0e44\u0e21\u0e48\u0e1e\u0e1a\u0e01\u0e32\u0e23\u0e2a\u0e31\u0e48\u0e19", + "on": "\u0e1e\u0e1a\u0e01\u0e32\u0e23\u0e2a\u0e31\u0e48\u0e19" + }, + "window": { + "off": "\u0e1b\u0e34\u0e14\u0e41\u0e25\u0e49\u0e27", + "on": "\u0e40\u0e1b\u0e34\u0e14" + } + }, + "title": "\u0e40\u0e0b\u0e47\u0e19\u0e40\u0e0b\u0e2d\u0e23\u0e4c\u0e41\u0e1a\u0e1a\u0e44\u0e1a\u0e19\u0e32\u0e23\u0e35" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/tr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/tr.json new file mode 100644 index 00000000000..daf44cc967b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/tr.json @@ -0,0 +1,107 @@ +{ + "device_automation": { + "trigger_type": { + "moist": "{entity_name} nemli oldu", + "not_opened": "{entity_name} kapat\u0131ld\u0131" + } + }, + "state": { + "_": { + "off": "Kapal\u0131", + "on": "A\u00e7\u0131k" + }, + "battery": { + "off": "Normal", + "on": "D\u00fc\u015f\u00fck" + }, + "battery_charging": { + "off": "\u015earj olmuyor", + "on": "\u015earj Oluyor" + }, + "cold": { + "off": "Normal", + "on": "So\u011fuk" + }, + "connectivity": { + "off": "Ba\u011flant\u0131 kesildi", + "on": "Ba\u011fl\u0131" + }, + "door": { + "off": "Kapal\u0131", + "on": "A\u00e7\u0131k" + }, + "garage_door": { + "off": "Kapal\u0131", + "on": "A\u00e7\u0131k" + }, + "gas": { + "off": "Temiz", + "on": "Alg\u0131land\u0131" + }, + "heat": { + "off": "Normal", + "on": "S\u0131cak" + }, + "light": { + "off": "I\u015f\u0131k yok", + "on": "I\u015f\u0131k alg\u0131land\u0131" + }, + "lock": { + "off": "Kilit kapal\u0131", + "on": "Kilit a\u00e7\u0131k" + }, + "moisture": { + "off": "Kuru", + "on": "Islak" + }, + "motion": { + "off": "Temiz", + "on": "Alg\u0131land\u0131" + }, + "moving": { + "off": "Hareket etmiyor", + "on": "Hareketli" + }, + "occupancy": { + "off": "Temiz", + "on": "Alg\u0131land\u0131" + }, + "opening": { + "off": "Kapal\u0131", + "on": "A\u00e7\u0131k" + }, + "plug": { + "off": "Fi\u015fi \u00e7ekildi", + "on": "Tak\u0131l\u0131" + }, + "presence": { + "off": "D\u0131\u015farda", + "on": "Evde" + }, + "problem": { + "off": "Tamam", + "on": "Sorun" + }, + "safety": { + "off": "G\u00fcvenli", + "on": "G\u00fcvensiz" + }, + "smoke": { + "off": "Temiz", + "on": "Alg\u0131land\u0131" + }, + "sound": { + "off": "Temiz", + "on": "Alg\u0131land\u0131" + }, + "vibration": { + "off": "Temiz", + "on": "Alg\u0131land\u0131" + }, + "window": { + "off": "Kapal\u0131", + "on": "A\u00e7\u0131k" + } + }, + "title": "\u0130kili sens\u00f6r" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/uk.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/uk.json new file mode 100644 index 00000000000..0f8d92749c4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/uk.json @@ -0,0 +1,191 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} \u043d\u0438\u0437\u044c\u043a\u0438\u0439 \u0440\u0456\u0432\u0435\u043d\u044c \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440\u0430", + "is_cold": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f", + "is_connected": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "is_gas": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0433\u0430\u0437", + "is_hot": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043d\u0430\u0433\u0440\u0456\u0432", + "is_light": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0441\u0432\u0456\u0442\u043b\u043e", + "is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_moist": "{entity_name} \u0432 \u0441\u0442\u0430\u043d\u0456 \"\u0412\u043e\u043b\u043e\u0433\u043e\"", + "is_motion": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0440\u0443\u0445", + "is_moving": "{entity_name} \u043f\u0435\u0440\u0435\u043c\u0456\u0449\u0443\u0454\u0442\u044c\u0441\u044f", + "is_no_gas": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0433\u0430\u0437", + "is_no_light": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0441\u0432\u0456\u0442\u043b\u043e", + "is_no_motion": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0440\u0443\u0445", + "is_no_problem": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", + "is_no_smoke": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0434\u0438\u043c", + "is_no_sound": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0437\u0432\u0443\u043a", + "is_no_vibration": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0432\u0456\u0431\u0440\u0430\u0446\u0456\u044e", + "is_not_bat_low": "{entity_name} \u0432 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_not_cold": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f", + "is_not_connected": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "is_not_hot": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043d\u0430\u0433\u0440\u0456\u0432", + "is_not_locked": "{entity_name} \u0432 \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_not_moist": "{entity_name} \u0432 \u0441\u0442\u0430\u043d\u0456 \"\u0421\u0443\u0445\u043e\"", + "is_not_moving": "{entity_name} \u043d\u0435 \u0440\u0443\u0445\u0430\u0454\u0442\u044c\u0441\u044f", + "is_not_occupied": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", + "is_not_open": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u0438\u0442\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_not_plugged_in": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "is_not_powered": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f", + "is_not_present": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", + "is_not_unsafe": "{entity_name} \u0432 \u0431\u0435\u0437\u043f\u0435\u0447\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_occupied": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", + "is_off": "{entity_name} \u0443 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_open": "{entity_name} \u0443 \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_plugged_in": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "is_powered": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f", + "is_present": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", + "is_problem": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "is_smoke": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0434\u0438\u043c", + "is_sound": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0437\u0432\u0443\u043a", + "is_unsafe": "{entity_name} \u0432 \u043d\u0435\u0431\u0435\u0437\u043f\u0435\u0447\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_vibration": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0432\u0456\u0431\u0440\u0430\u0446\u0456\u044e" + }, + "trigger_type": { + "bat_low": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043d\u0438\u0437\u044c\u043a\u0438\u0439 \u0437\u0430\u0440\u044f\u0434", + "cold": "{entity_name} \u043e\u0445\u043e\u043b\u043e\u0434\u0436\u0443\u0454\u0442\u044c\u0441\u044f", + "connected": "{entity_name} \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0430\u0454\u0442\u044c\u0441\u044f", + "gas": "{entity_name} \u043f\u043e\u0447\u0438\u043d\u0430\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0433\u0430\u0437", + "hot": "{entity_name} \u043d\u0430\u0433\u0440\u0456\u0432\u0430\u0454\u0442\u044c\u0441\u044f", + "light": "{entity_name} \u043f\u043e\u0447\u0438\u043d\u0430\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0441\u0432\u0456\u0442\u043b\u043e", + "locked": "{entity_name} \u0431\u043b\u043e\u043a\u0443\u0454\u0442\u044c\u0441\u044f", + "moist": "{entity_name} \u0441\u0442\u0430\u0454 \u0432\u043e\u043b\u043e\u0433\u0438\u043c", + "motion": "{entity_name} \u043f\u043e\u0447\u0438\u043d\u0430\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0440\u0443\u0445", + "moving": "{entity_name} \u043f\u043e\u0447\u0438\u043d\u0430\u0454 \u0440\u0443\u0445\u0430\u0442\u0438\u0441\u044f", + "no_gas": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0433\u0430\u0437", + "no_light": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0441\u0432\u0456\u0442\u043b\u043e", + "no_motion": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0440\u0443\u0445", + "no_problem": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "no_smoke": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0434\u0438\u043c", + "no_sound": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0437\u0432\u0443\u043a", + "no_vibration": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0432\u0456\u0431\u0440\u0430\u0446\u0456\u044e", + "not_bat_low": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u0438\u0439 \u0437\u0430\u0440\u044f\u0434", + "not_cold": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u043e\u0445\u043e\u043b\u043e\u0434\u0436\u0443\u0432\u0430\u0442\u0438\u0441\u044f", + "not_connected": "{entity_name} \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0430\u0454\u0442\u044c\u0441\u044f", + "not_hot": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u043d\u0430\u0433\u0440\u0456\u0432\u0430\u0442\u0438\u0441\u044f", + "not_locked": "{entity_name} \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0454\u0442\u044c\u0441\u044f", + "not_moist": "{entity_name} \u0441\u0442\u0430\u0454 \u0441\u0443\u0445\u0438\u043c", + "not_moving": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u043f\u0435\u0440\u0435\u043c\u0456\u0449\u0435\u043d\u043d\u044f", + "not_occupied": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", + "not_opened": "{entity_name} \u0437\u0430\u043a\u0440\u0438\u0442\u043e", + "not_plugged_in": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "not_powered": "{entity_name} \u043d\u0435 \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043d\u0430\u044f\u0432\u043d\u0456\u0441\u0442\u044c \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f", + "not_present": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u0432\u0456\u0434\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", + "not_unsafe": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u0431\u0435\u0437\u043f\u0435\u043a\u0443", + "occupied": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", + "opened": "{entity_name} \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u043e", + "plugged_in": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "powered": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043d\u0430\u044f\u0432\u043d\u0456\u0441\u0442\u044c \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f", + "present": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", + "problem": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "smoke": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0434\u0438\u043c", + "sound": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0437\u0432\u0443\u043a", + "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e", + "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e", + "unsafe": "{entity_name} \u043d\u0435 \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u0431\u0435\u0437\u043f\u0435\u043a\u0443", + "vibration": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0432\u0456\u0431\u0440\u0430\u0446\u0456\u044e" + } + }, + "state": { + "_": { + "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e", + "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" + }, + "battery": { + "off": "\u041d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u0438\u0439", + "on": "\u041d\u0438\u0437\u044c\u043a\u0438\u0439" + }, + "battery_charging": { + "off": "\u041d\u0435 \u0437\u0430\u0440\u044f\u0434\u0436\u0430\u0454\u0442\u044c\u0441\u044f", + "on": "\u0417\u0430\u0440\u044f\u0434\u0436\u0430\u043d\u043d\u044f" + }, + "cold": { + "off": "\u041d\u043e\u0440\u043c\u0430", + "on": "\u041e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f" + }, + "connectivity": { + "off": "\u0412\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + }, + "door": { + "off": "\u0417\u0430\u0447\u0438\u043d\u0435\u043d\u0456", + "on": "\u0412\u0456\u0434\u0447\u0438\u043d\u0435\u043d\u0456" + }, + "garage_door": { + "off": "\u0417\u0430\u0447\u0438\u043d\u0435\u043d\u0406", + "on": "\u0412\u0456\u0434\u043a\u0440\u0438\u0442\u0456" + }, + "gas": { + "off": "\u0427\u0438\u0441\u0442\u043e", + "on": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u0433\u0430\u0437" + }, + "heat": { + "off": "\u041d\u043e\u0440\u043c\u0430", + "on": "\u041d\u0430\u0433\u0440\u0456\u0432\u0430\u043d\u043d\u044f" + }, + "light": { + "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e", + "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" + }, + "lock": { + "off": "\u0417\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e", + "on": "\u0420\u043e\u0437\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e" + }, + "moisture": { + "off": "\u0421\u0443\u0445\u043e", + "on": "\u0412\u043e\u043b\u043e\u0433\u043e" + }, + "motion": { + "off": "\u041d\u0435\u043c\u0430\u0454 \u0440\u0443\u0445\u0443", + "on": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u0440\u0443\u0445" + }, + "moving": { + "off": "\u0420\u0443\u0445\u0443 \u043d\u0435\u043c\u0430\u0454", + "on": "\u0420\u0443\u0445\u0430\u0454\u0442\u044c\u0441\u044f" + }, + "occupancy": { + "off": "\u0427\u0438\u0441\u0442\u043e", + "on": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c" + }, + "opening": { + "off": "\u0417\u0430\u0447\u0438\u043d\u0435\u043d\u043e", + "on": "\u0412\u0456\u0434\u0447\u0438\u043d\u0435\u043d\u043e" + }, + "plug": { + "off": "\u0412\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + }, + "presence": { + "off": "\u041d\u0435 \u0432\u0434\u043e\u043c\u0430", + "on": "\u0412\u0434\u043e\u043c\u0430" + }, + "problem": { + "off": "\u041e\u041a", + "on": "\u041f\u0440\u043e\u0431\u043b\u0435\u043c\u0430" + }, + "safety": { + "off": "\u0411\u0435\u0437\u043f\u0435\u0447\u043d\u043e", + "on": "\u041d\u0435\u0431\u0435\u0437\u043f\u0435\u0447\u043d\u043e" + }, + "smoke": { + "off": "\u0427\u0438\u0441\u0442\u043e", + "on": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u0434\u0438\u043c" + }, + "sound": { + "off": "\u0427\u0438\u0441\u0442\u043e", + "on": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u0437\u0432\u0443\u043a" + }, + "vibration": { + "off": "\u041d\u0435 \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043e", + "on": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u0430 \u0432\u0456\u0431\u0440\u0430\u0446\u0456\u044f" + }, + "window": { + "off": "\u0417\u0430\u0447\u0438\u043d\u0435\u043d\u043e", + "on": "\u0412\u0456\u0434\u0447\u0438\u043d\u0435\u043d\u043e" + } + }, + "title": "\u0411\u0456\u043d\u0430\u0440\u043d\u0438\u0439 \u0434\u0430\u0442\u0447\u0438\u043a" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/vi.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/vi.json new file mode 100644 index 00000000000..d74bda46730 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/vi.json @@ -0,0 +1,85 @@ +{ + "state": { + "_": { + "off": "T\u1eaft", + "on": "B\u1eadt" + }, + "battery": { + "off": "B\u00ecnh th\u01b0\u1eddng", + "on": "Th\u1ea5p" + }, + "cold": { + "off": "B\u00ecnh th\u01b0\u1eddng", + "on": "L\u1ea1nh" + }, + "connectivity": { + "off": "\u0110\u00e3 ng\u1eaft k\u1ebft n\u1ed1i", + "on": "\u0110\u00e3 k\u1ebft n\u1ed1i" + }, + "door": { + "off": "\u0110\u00f3ng", + "on": "M\u1edf" + }, + "garage_door": { + "off": "\u0110\u00f3ng", + "on": "M\u1edf" + }, + "gas": { + "off": "Tr\u00f4\u0341ng tra\u0309i", + "on": "Ph\u00e1t hi\u1ec7n" + }, + "heat": { + "off": "B\u00ecnh th\u01b0\u1eddng", + "on": "N\u00f3ng" + }, + "lock": { + "off": "\u0110\u00e3 kho\u00e1", + "on": "M\u1edf kho\u00e1" + }, + "moisture": { + "off": "Kh\u00f4", + "on": "\u01af\u1edbt" + }, + "motion": { + "off": "Tr\u00f4\u0341ng tra\u0309i", + "on": "Ph\u00e1t hi\u1ec7n" + }, + "occupancy": { + "off": "Tr\u00f4\u0341ng tra\u0309i", + "on": "Ph\u00e1t hi\u1ec7n" + }, + "opening": { + "off": "\u0110\u00e3 \u0111\u00f3ng", + "on": "M\u1edf" + }, + "presence": { + "off": "\u0110i v\u1eafng", + "on": "\u1ede nh\u00e0" + }, + "problem": { + "off": "OK", + "on": "C\u00f3 v\u1ea5n \u0111\u1ec1" + }, + "safety": { + "off": "An to\u00e0n", + "on": "Kh\u00f4ng an to\u00e0n" + }, + "smoke": { + "off": "Tr\u00f4\u0341ng tra\u0309i", + "on": "Ph\u00e1t hi\u1ec7n" + }, + "sound": { + "off": "Tr\u00f4\u0341ng tra\u0309i", + "on": "Ph\u00e1t hi\u1ec7n" + }, + "vibration": { + "off": "Tr\u00f4\u0341ng tra\u0309i", + "on": "Ph\u00e1t hi\u1ec7n" + }, + "window": { + "off": "\u0110\u00f3ng", + "on": "M\u1edf" + } + }, + "title": "C\u1ea3m bi\u1ebfn nh\u1ecb ph\u00e2n" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/zh-Hans.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/zh-Hans.json new file mode 100644 index 00000000000..82cd0d3ccfe --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/zh-Hans.json @@ -0,0 +1,191 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} \u7535\u6c60\u7535\u91cf\u4f4e", + "is_cold": "{entity_name} \u8fc7\u51b7", + "is_connected": "{entity_name} \u5df2\u8fde\u63a5", + "is_gas": "{entity_name} \u68c0\u6d4b\u5230\u71c3\u6c14\u6cc4\u6f0f", + "is_hot": "{entity_name} \u8fc7\u70ed", + "is_light": "{entity_name} \u68c0\u6d4b\u5230\u5149\u7ebf", + "is_locked": "{entity_name} \u5df2\u9501\u5b9a", + "is_moist": "{entity_name} \u6f6e\u6e7f", + "is_motion": "{entity_name} \u68c0\u6d4b\u5230\u6709\u4eba", + "is_moving": "{entity_name} \u6b63\u5728\u79fb\u52a8", + "is_no_gas": "{entity_name} \u672a\u68c0\u6d4b\u5230\u71c3\u6c14\u6cc4\u6f0f", + "is_no_light": "{entity_name} \u672a\u68c0\u6d4b\u5230\u5149\u7ebf", + "is_no_motion": "{entity_name} \u672a\u68c0\u6d4b\u5230\u6709\u4eba", + "is_no_problem": "{entity_name} \u672a\u53d1\u73b0\u95ee\u9898", + "is_no_smoke": "{entity_name} \u672a\u68c0\u6d4b\u5230\u70df\u96fe", + "is_no_sound": "{entity_name} \u672a\u68c0\u6d4b\u5230\u58f0\u97f3", + "is_no_vibration": "{entity_name} \u672a\u68c0\u6d4b\u5230\u632f\u52a8", + "is_not_bat_low": "{entity_name} \u7535\u6c60\u7535\u91cf\u6b63\u5e38", + "is_not_cold": "{entity_name} \u4e0d\u51b7", + "is_not_connected": "{entity_name} \u5df2\u65ad\u5f00", + "is_not_hot": "{entity_name} \u4e0d\u70ed", + "is_not_locked": "{entity_name} \u5df2\u89e3\u9501", + "is_not_moist": "{entity_name} \u5e72\u71e5", + "is_not_moving": "{entity_name} \u9759\u6b62", + "is_not_occupied": "{entity_name} \u65e0\u4eba", + "is_not_open": "{entity_name} \u5df2\u5173\u95ed", + "is_not_plugged_in": "{entity_name} \u672a\u63d2\u5165", + "is_not_powered": "{entity_name} \u672a\u901a\u7535", + "is_not_present": "{entity_name} \u4e0d\u5728\u5bb6", + "is_not_unsafe": "{entity_name} \u5b89\u5168", + "is_occupied": "{entity_name} \u6709\u4eba", + "is_off": "{entity_name} \u5df2\u5173\u95ed", + "is_on": "{entity_name} \u5df2\u5f00\u542f", + "is_open": "{entity_name} \u5df2\u6253\u5f00", + "is_plugged_in": "{entity_name} \u5df2\u63d2\u5165", + "is_powered": "{entity_name} \u5df2\u901a\u7535", + "is_present": "{entity_name} \u5728\u5bb6", + "is_problem": "{entity_name} \u53d1\u73b0\u95ee\u9898", + "is_smoke": "{entity_name} \u68c0\u6d4b\u5230\u70df\u96fe", + "is_sound": "{entity_name} \u68c0\u6d4b\u5230\u58f0\u97f3", + "is_unsafe": "{entity_name} \u4e0d\u5b89\u5168", + "is_vibration": "{entity_name} \u68c0\u6d4b\u5230\u632f\u52a8" + }, + "trigger_type": { + "bat_low": "{entity_name} \u7535\u6c60\u7535\u91cf\u4f4e", + "cold": "{entity_name} \u53d8\u51b7", + "connected": "{entity_name} \u5df2\u8fde\u63a5", + "gas": "{entity_name} \u5f00\u59cb\u68c0\u6d4b\u5230\u71c3\u6c14\u6cc4\u6f0f", + "hot": "{entity_name} \u53d8\u70ed", + "light": "{entity_name} \u5f00\u59cb\u68c0\u6d4b\u5230\u5149\u7ebf", + "locked": "{entity_name} \u88ab\u9501\u5b9a", + "moist": "{entity_name} \u53d8\u6e7f", + "motion": "{entity_name} \u68c0\u6d4b\u5230\u6709\u4eba", + "moving": "{entity_name} \u5f00\u59cb\u79fb\u52a8", + "no_gas": "{entity_name} \u4e0d\u518d\u68c0\u6d4b\u5230\u71c3\u6c14\u6cc4\u6f0f", + "no_light": "{entity_name} \u4e0d\u518d\u68c0\u6d4b\u5230\u5149\u7ebf", + "no_motion": "{entity_name} \u4e0d\u518d\u68c0\u6d4b\u5230\u6709\u4eba", + "no_problem": "{entity_name} \u95ee\u9898\u89e3\u9664", + "no_smoke": "{entity_name} \u4e0d\u518d\u68c0\u6d4b\u5230\u70df\u96fe", + "no_sound": "{entity_name} \u4e0d\u518d\u68c0\u6d4b\u5230\u58f0\u97f3", + "no_vibration": "{entity_name} \u4e0d\u518d\u68c0\u6d4b\u5230\u632f\u52a8", + "not_bat_low": "{entity_name} \u7535\u6c60\u7535\u91cf\u6b63\u5e38", + "not_cold": "{entity_name} \u4e0d\u51b7\u4e86", + "not_connected": "{entity_name} \u65ad\u5f00", + "not_hot": "{entity_name} \u4e0d\u70ed\u4e86", + "not_locked": "{entity_name} \u89e3\u9501", + "not_moist": "{entity_name} \u53d8\u5e72", + "not_moving": "{entity_name} \u505c\u6b62\u79fb\u52a8", + "not_occupied": "{entity_name} \u4e0d\u518d\u6709\u4eba", + "not_opened": "{entity_name} \u5df2\u5173\u95ed", + "not_plugged_in": "{entity_name} \u88ab\u62d4\u51fa", + "not_powered": "{entity_name} \u6389\u7535", + "not_present": "{entity_name} \u4e0d\u5728\u5bb6", + "not_unsafe": "{entity_name} \u5b89\u5168\u4e86", + "occupied": "{entity_name} \u6709\u4eba", + "opened": "{entity_name} \u88ab\u6253\u5f00", + "plugged_in": "{entity_name} \u88ab\u63d2\u5165", + "powered": "{entity_name} \u4e0a\u7535", + "present": "{entity_name} \u5728\u5bb6", + "problem": "{entity_name} \u53d1\u73b0\u95ee\u9898", + "smoke": "{entity_name} \u5f00\u59cb\u68c0\u6d4b\u5230\u70df\u96fe", + "sound": "{entity_name} \u5f00\u59cb\u68c0\u6d4b\u5230\u58f0\u97f3", + "turned_off": "{entity_name} \u88ab\u5173\u95ed", + "turned_on": "{entity_name} \u88ab\u6253\u5f00", + "unsafe": "{entity_name} \u4e0d\u518d\u5b89\u5168", + "vibration": "{entity_name} \u5f00\u59cb\u68c0\u6d4b\u5230\u632f\u52a8" + } + }, + "state": { + "_": { + "off": "\u5173\u95ed", + "on": "\u5f00\u542f" + }, + "battery": { + "off": "\u6b63\u5e38", + "on": "\u4f4e" + }, + "battery_charging": { + "off": "\u672a\u5728\u5145\u7535", + "on": "\u6b63\u5728\u5145\u7535" + }, + "cold": { + "off": "\u6b63\u5e38", + "on": "\u8fc7\u51b7" + }, + "connectivity": { + "off": "\u5df2\u65ad\u5f00", + "on": "\u5df2\u8fde\u63a5" + }, + "door": { + "off": "\u5173\u95ed", + "on": "\u5f00\u542f" + }, + "garage_door": { + "off": "\u5173\u95ed", + "on": "\u5f00\u542f" + }, + "gas": { + "off": "\u6b63\u5e38", + "on": "\u89e6\u53d1" + }, + "heat": { + "off": "\u6b63\u5e38", + "on": "\u8fc7\u70ed" + }, + "light": { + "off": "\u65e0\u5149", + "on": "\u6709\u5149" + }, + "lock": { + "off": "\u4e0a\u9501", + "on": "\u89e3\u9501" + }, + "moisture": { + "off": "\u5e72\u71e5", + "on": "\u6e7f\u6da6" + }, + "motion": { + "off": "\u672a\u89e6\u53d1", + "on": "\u89e6\u53d1" + }, + "moving": { + "off": "\u9759\u6b62", + "on": "\u6b63\u5728\u79fb\u52a8" + }, + "occupancy": { + "off": "\u65e0\u4eba", + "on": "\u6709\u4eba" + }, + "opening": { + "off": "\u5173\u95ed", + "on": "\u5f00\u542f" + }, + "plug": { + "off": "\u5df2\u62d4\u51fa", + "on": "\u5df2\u63d2\u5165" + }, + "presence": { + "off": "\u79bb\u5f00", + "on": "\u5728\u5bb6" + }, + "problem": { + "off": "\u6b63\u5e38", + "on": "\u5f02\u5e38" + }, + "safety": { + "off": "\u5b89\u5168", + "on": "\u5371\u9669" + }, + "smoke": { + "off": "\u6b63\u5e38", + "on": "\u89e6\u53d1" + }, + "sound": { + "off": "\u6b63\u5e38", + "on": "\u89e6\u53d1" + }, + "vibration": { + "off": "\u6b63\u5e38", + "on": "\u89e6\u53d1" + }, + "window": { + "off": "\u5173\u95ed", + "on": "\u5f00\u542f" + } + }, + "title": "\u4e8c\u5143\u4f20\u611f\u5668" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/zh-Hant.json b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/zh-Hant.json new file mode 100644 index 00000000000..bf50782743e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/binary_sensor/translations/zh-Hant.json @@ -0,0 +1,191 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name}\u96fb\u91cf\u904e\u4f4e", + "is_cold": "{entity_name}\u51b7", + "is_connected": "{entity_name}\u5df2\u9023\u7dda", + "is_gas": "{entity_name}\u5075\u6e2c\u5230\u6c23\u9ad4", + "is_hot": "{entity_name}\u71b1", + "is_light": "{entity_name}\u5075\u6e2c\u5230\u5149\u7dda\u4e2d", + "is_locked": "{entity_name}\u5df2\u4e0a\u9396", + "is_moist": "{entity_name}\u6f6e\u6fd5", + "is_motion": "{entity_name}\u5075\u6e2c\u5230\u52d5\u4f5c\u4e2d", + "is_moving": "{entity_name}\u79fb\u52d5\u4e2d", + "is_no_gas": "{entity_name}\u672a\u5075\u6e2c\u5230\u6c23\u9ad4", + "is_no_light": "{entity_name}\u672a\u5075\u6e2c\u5230\u5149\u7dda", + "is_no_motion": "{entity_name}\u672a\u5075\u6e2c\u5230\u52d5\u4f5c", + "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_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", + "is_not_connected": "{entity_name}\u65b7\u7dda", + "is_not_hot": "{entity_name}\u4e0d\u71b1", + "is_not_locked": "{entity_name}\u89e3\u9396", + "is_not_moist": "{entity_name}\u4e7e\u71e5", + "is_not_moving": "{entity_name}\u672a\u5728\u79fb\u52d5", + "is_not_occupied": "{entity_name}\u672a\u6709\u4eba", + "is_not_open": "{entity_name}\u95dc\u9589", + "is_not_plugged_in": "{entity_name}\u672a\u63d2\u5165", + "is_not_powered": "{entity_name}\u672a\u901a\u96fb", + "is_not_present": "{entity_name}\u672a\u51fa\u73fe", + "is_not_unsafe": "{entity_name}\u5b89\u5168", + "is_occupied": "{entity_name}\u6709\u4eba", + "is_off": "{entity_name}\u95dc\u9589", + "is_on": "{entity_name}\u958b\u555f", + "is_open": "{entity_name}\u958b\u555f", + "is_plugged_in": "{entity_name}\u63d2\u5165", + "is_powered": "{entity_name}\u901a\u96fb", + "is_present": "{entity_name}\u51fa\u73fe", + "is_problem": "{entity_name}\u6b63\u5075\u6e2c\u5230\u554f\u984c", + "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_vibration": "{entity_name}\u6b63\u5075\u6e2c\u5230\u9707\u52d5" + }, + "trigger_type": { + "bat_low": "{entity_name}\u96fb\u91cf\u4f4e", + "cold": "{entity_name}\u5df2\u8b8a\u51b7", + "connected": "{entity_name}\u5df2\u9023\u7dda", + "gas": "{entity_name}\u5df2\u958b\u59cb\u5075\u6e2c\u6c23\u9ad4", + "hot": "{entity_name}\u5df2\u8b8a\u71b1", + "light": "{entity_name}\u5df2\u958b\u59cb\u5075\u6e2c\u5149\u7dda", + "locked": "{entity_name}\u5df2\u4e0a\u9396", + "moist": "{entity_name}\u5df2\u8b8a\u6f6e\u6fd5", + "motion": "{entity_name}\u5df2\u5075\u6e2c\u5230\u52d5\u4f5c", + "moving": "{entity_name}\u958b\u59cb\u79fb\u52d5", + "no_gas": "{entity_name}\u5df2\u505c\u6b62\u5075\u6e2c\u6c23\u9ad4", + "no_light": "{entity_name}\u5df2\u505c\u6b62\u5075\u6e2c\u5149\u7dda", + "no_motion": "{entity_name}\u5df2\u505c\u6b62\u5075\u6e2c\u52d5\u4f5c", + "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_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", + "not_connected": "{entity_name}\u5df2\u65b7\u7dda", + "not_hot": "{entity_name}\u5df2\u4e0d\u71b1", + "not_locked": "{entity_name}\u5df2\u89e3\u9396", + "not_moist": "{entity_name}\u5df2\u8b8a\u4e7e", + "not_moving": "{entity_name}\u505c\u6b62\u79fb\u52d5", + "not_occupied": "{entity_name}\u672a\u6709\u4eba", + "not_opened": "{entity_name}\u5df2\u95dc\u9589", + "not_plugged_in": "{entity_name}\u672a\u63d2\u5165", + "not_powered": "{entity_name}\u672a\u901a\u96fb", + "not_present": "{entity_name}\u672a\u51fa\u73fe", + "not_unsafe": "{entity_name}\u5df2\u5b89\u5168", + "occupied": "{entity_name}\u8b8a\u6210\u6709\u4eba", + "opened": "{entity_name}\u5df2\u958b\u555f", + "plugged_in": "{entity_name}\u5df2\u63d2\u5165", + "powered": "{entity_name}\u5df2\u901a\u96fb", + "present": "{entity_name}\u5df2\u51fa\u73fe", + "problem": "{entity_name}\u5df2\u5075\u6e2c\u5230\u554f\u984c", + "smoke": "{entity_name}\u5df2\u5075\u6e2c\u5230\u7159\u9727", + "sound": "{entity_name}\u5df2\u5075\u6e2c\u5230\u8072\u97f3", + "turned_off": "{entity_name}\u5df2\u95dc\u9589", + "turned_on": "{entity_name}\u5df2\u958b\u555f", + "unsafe": "{entity_name}\u5df2\u4e0d\u5b89\u5168", + "vibration": "{entity_name}\u5df2\u5075\u6e2c\u5230\u9707\u52d5" + } + }, + "state": { + "_": { + "off": "\u95dc\u9589", + "on": "\u958b\u555f" + }, + "battery": { + "off": "\u96fb\u91cf\u6b63\u5e38", + "on": "\u96fb\u91cf\u4f4e" + }, + "battery_charging": { + "off": "\u672a\u5728\u5145\u96fb", + "on": "\u5145\u96fb\u4e2d" + }, + "cold": { + "off": "\u6b63\u5e38", + "on": "\u51b7" + }, + "connectivity": { + "off": "\u5df2\u65b7\u7dda", + "on": "\u5df2\u9023\u7dda" + }, + "door": { + "off": "\u5df2\u95dc\u9589", + "on": "\u5df2\u958b\u555f" + }, + "garage_door": { + "off": "\u95dc\u9589", + "on": "\u5df2\u958b\u555f" + }, + "gas": { + "off": "\u672a\u89f8\u767c", + "on": "\u5df2\u89f8\u767c" + }, + "heat": { + "off": "\u6b63\u5e38", + "on": "\u71b1" + }, + "light": { + "off": "\u6c92\u6709\u5149\u7dda", + "on": "\u5075\u6e2c\u5230\u5149\u7dda" + }, + "lock": { + "off": "\u5df2\u4e0a\u9396", + "on": "\u5df2\u89e3\u9396" + }, + "moisture": { + "off": "\u4e7e\u71e5", + "on": "\u6fd5\u6f64" + }, + "motion": { + "off": "\u7121\u4eba", + "on": "\u6709\u4eba" + }, + "moving": { + "off": "\u672a\u5728\u79fb\u52d5", + "on": "\u79fb\u52d5\u4e2d" + }, + "occupancy": { + "off": "\u672a\u89f8\u767c", + "on": "\u5df2\u89f8\u767c" + }, + "opening": { + "off": "\u95dc\u9589", + "on": "\u958b\u555f" + }, + "plug": { + "off": "\u5df2\u62d4\u9664", + "on": "\u5df2\u63d2\u4e0a" + }, + "presence": { + "off": "\u96e2\u5bb6", + "on": "\u5728\u5bb6" + }, + "problem": { + "off": "\u78ba\u5b9a", + "on": "\u7570\u5e38" + }, + "safety": { + "off": "\u5b89\u5168", + "on": "\u5371\u96aa" + }, + "smoke": { + "off": "\u672a\u89f8\u767c", + "on": "\u5df2\u89f8\u767c" + }, + "sound": { + "off": "\u672a\u89f8\u767c", + "on": "\u5df2\u89f8\u767c" + }, + "vibration": { + "off": "\u672a\u5075\u6e2c", + "on": "\u5075\u6e2c" + }, + "window": { + "off": "\u95dc\u9589", + "on": "\u958b\u555f" + } + }, + "title": "\u4e8c\u9032\u4f4d\u50b3\u611f\u5668" +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/bitcoin/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/bitcoin/__init__.py new file mode 100644 index 00000000000..cfdfb53c044 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/bitcoin/__init__.py @@ -0,0 +1 @@ +"""The bitcoin component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/bitcoin/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/bitcoin/manifest.json new file mode 100644 index 00000000000..0a8abfa6500 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/bitcoin/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "bitcoin", + "name": "Bitcoin", + "documentation": "https://www.home-assistant.io/integrations/bitcoin", + "requirements": ["blockchain==1.4.4"], + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/bitcoin/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/bitcoin/sensor.py new file mode 100644 index 00000000000..4acce03d6fa --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/bitcoin/sensor.py @@ -0,0 +1,179 @@ +"""Bitcoin information service that uses blockchain.com.""" +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.const import ( + ATTR_ATTRIBUTION, + CONF_CURRENCY, + CONF_DISPLAY_OPTIONS, + TIME_MINUTES, + TIME_SECONDS, +) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = "Data provided by blockchain.com" + +DEFAULT_CURRENCY = "USD" + +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"], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_DISPLAY_OPTIONS, default=[]): vol.All( + cv.ensure_list, [vol.In(OPTION_TYPES)] + ), + vol.Optional(CONF_CURRENCY, default=DEFAULT_CURRENCY): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Bitcoin sensors.""" + + currency = config[CONF_CURRENCY] + + if currency not in exchangerates.get_ticker(): + _LOGGER.warning("Currency %s is not available. Using USD", currency) + currency = DEFAULT_CURRENCY + + data = BitcoinData() + dev = [] + for variable in config[CONF_DISPLAY_OPTIONS]: + dev.append(BitcoinSensor(data, variable, currency)) + + add_entities(dev, True) + + +class BitcoinSensor(SensorEntity): + """Representation of a Bitcoin sensor.""" + + def __init__(self, data, option_type, currency): + """Initialize the sensor.""" + self.data = data + self._name = OPTION_TYPES[option_type][0] + self._unit_of_measurement = OPTION_TYPES[option_type][1] + self._currency = currency + self.type = option_type + self._state = None + + @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._state + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit_of_measurement + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return ICON + + @property + def extra_state_attributes(self): + """Return the state attributes of the sensor.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + def update(self): + """Get the latest data and updates the states.""" + self.data.update() + stats = self.data.stats + ticker = self.data.ticker + + if self.type == "exchangerate": + self._state = ticker[self._currency].p15min + self._unit_of_measurement = self._currency + elif self.type == "trade_volume_btc": + self._state = f"{stats.trade_volume_btc:.1f}" + elif self.type == "miners_revenue_usd": + self._state = f"{stats.miners_revenue_usd:.0f}" + elif self.type == "btc_mined": + self._state = str(stats.btc_mined * 0.00000001) + elif self.type == "trade_volume_usd": + self._state = f"{stats.trade_volume_usd:.1f}" + elif self.type == "difficulty": + self._state = f"{stats.difficulty:.0f}" + elif self.type == "minutes_between_blocks": + self._state = f"{stats.minutes_between_blocks:.2f}" + elif self.type == "number_of_transactions": + self._state = str(stats.number_of_transactions) + elif self.type == "hash_rate": + self._state = f"{stats.hash_rate * 0.000001:.1f}" + elif self.type == "timestamp": + self._state = stats.timestamp + elif self.type == "mined_blocks": + self._state = str(stats.mined_blocks) + elif self.type == "blocks_size": + self._state = f"{stats.blocks_size:.1f}" + elif self.type == "total_fees_btc": + self._state = f"{stats.total_fees_btc * 0.00000001:.2f}" + elif self.type == "total_btc_sent": + self._state = f"{stats.total_btc_sent * 0.00000001:.2f}" + elif self.type == "estimated_btc_sent": + self._state = f"{stats.estimated_btc_sent * 0.00000001:.2f}" + elif self.type == "total_btc": + self._state = f"{stats.total_btc * 0.00000001:.2f}" + elif self.type == "total_blocks": + self._state = f"{stats.total_blocks:.0f}" + elif self.type == "next_retarget": + self._state = f"{stats.next_retarget:.2f}" + elif self.type == "estimated_transaction_volume_usd": + self._state = f"{stats.estimated_transaction_volume_usd:.2f}" + elif self.type == "miners_revenue_btc": + self._state = f"{stats.miners_revenue_btc * 0.00000001:.1f}" + elif self.type == "market_price_usd": + self._state = f"{stats.market_price_usd:.2f}" + + +class BitcoinData: + """Get the latest data and update the states.""" + + def __init__(self): + """Initialize the data object.""" + self.stats = None + self.ticker = None + + def update(self): + """Get the latest data from blockchain.com.""" + + self.stats = statistics.get() + self.ticker = exchangerates.get_ticker() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/bizkaibus/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/bizkaibus/__init__.py new file mode 100644 index 00000000000..e37c17e5744 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/bizkaibus/__init__.py @@ -0,0 +1 @@ +"""The Bizkaibus bus tracker component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/bizkaibus/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/bizkaibus/manifest.json new file mode 100644 index 00000000000..c8923f3d541 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/bizkaibus/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "bizkaibus", + "name": "Bizkaibus", + "documentation": "https://www.home-assistant.io/integrations/bizkaibus", + "codeowners": ["@UgaitzEtxebarria"], + "requirements": ["bizkaibus==0.1.1"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/bizkaibus/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/bizkaibus/sensor.py new file mode 100644 index 00000000000..d0cade31a72 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/bizkaibus/sensor.py @@ -0,0 +1,83 @@ +"""Support for Bizkaibus, Biscay (Basque Country, Spain) Bus service.""" +from contextlib import suppress + +from bizkaibus.bizkaibus import BizkaibusData +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.const import CONF_NAME, TIME_MINUTES +import homeassistant.helpers.config_validation as cv + +ATTR_DUE_IN = "Due in" + +CONF_STOP_ID = "stopid" +CONF_ROUTE = "route" + +DEFAULT_NAME = "Next bus" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_STOP_ID): cv.string, + vol.Required(CONF_ROUTE): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Bizkaibus public transport sensor.""" + name = config[CONF_NAME] + stop = config[CONF_STOP_ID] + route = config[CONF_ROUTE] + + data = Bizkaibus(stop, route) + add_entities([BizkaibusSensor(data, stop, route, name)], True) + + +class BizkaibusSensor(SensorEntity): + """The class for handling the data.""" + + def __init__(self, data, stop, route, name): + """Initialize the sensor.""" + self.data = data + self.stop = stop + self.route = route + self._name = name + self._state = None + + @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._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of the sensor.""" + return TIME_MINUTES + + def update(self): + """Get the latest data from the webservice.""" + self.data.update() + with suppress(TypeError): + self._state = self.data.info[0][ATTR_DUE_IN] + + +class Bizkaibus: + """The class for handling the data retrieval.""" + + def __init__(self, stop, route): + """Initialize the data object.""" + self.stop = stop + self.route = route + self.info = None + + def update(self): + """Retrieve the information from API.""" + bridge = BizkaibusData(self.stop, self.route) + bridge.getNextBus() + self.info = bridge.info diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blackbird/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/blackbird/__init__.py new file mode 100644 index 00000000000..b901bda0469 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blackbird/__init__.py @@ -0,0 +1 @@ +"""The blackbird component.""" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blackbird/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/blackbird/const.py new file mode 100644 index 00000000000..aa8d7e7d514 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blackbird/const.py @@ -0,0 +1,3 @@ +"""Constants for the Monoprice Blackbird Matrix Switch component.""" +DOMAIN = "blackbird" +SERVICE_SETALLZONES = "set_all_zones" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blackbird/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/blackbird/manifest.json new file mode 100644 index 00000000000..04bde4b4617 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blackbird/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "blackbird", + "name": "Monoprice Blackbird Matrix Switch", + "documentation": "https://www.home-assistant.io/integrations/blackbird", + "requirements": ["pyblackbird==0.5"], + "codeowners": [], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blackbird/media_player.py b/homeassistant-2021.6.0.dev0/homeassistant/components/blackbird/media_player.py new file mode 100644 index 00000000000..9ae696a5276 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blackbird/media_player.py @@ -0,0 +1,216 @@ +"""Support for interfacing with Monoprice Blackbird 4k 8x8 HDBaseT Matrix.""" +import logging +import socket + +from pyblackbird import get_blackbird +from serial import SerialException +import voluptuous as vol + +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity +from homeassistant.components.media_player.const import ( + SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_TYPE, + STATE_OFF, + STATE_ON, +) +import homeassistant.helpers.config_validation as cv + +from .const import DOMAIN, SERVICE_SETALLZONES + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_BLACKBIRD = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE + +MEDIA_PLAYER_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.comp_entity_ids}) + +ZONE_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string}) + +SOURCE_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string}) + +CONF_ZONES = "zones" +CONF_SOURCES = "sources" + +DATA_BLACKBIRD = "blackbird" + +ATTR_SOURCE = "source" + +BLACKBIRD_SETALLZONES_SCHEMA = MEDIA_PLAYER_SCHEMA.extend( + {vol.Required(ATTR_SOURCE): cv.string} +) + + +# Valid zone ids: 1-8 +ZONE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=8)) + +# Valid source ids: 1-8 +SOURCE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=8)) + +PLATFORM_SCHEMA = vol.All( + cv.has_at_least_one_key(CONF_PORT, CONF_HOST), + PLATFORM_SCHEMA.extend( + { + vol.Exclusive(CONF_PORT, CONF_TYPE): cv.string, + vol.Exclusive(CONF_HOST, CONF_TYPE): cv.string, + vol.Required(CONF_ZONES): vol.Schema({ZONE_IDS: ZONE_SCHEMA}), + vol.Required(CONF_SOURCES): vol.Schema({SOURCE_IDS: SOURCE_SCHEMA}), + } + ), +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Monoprice Blackbird 4k 8x8 HDBaseT Matrix platform.""" + if DATA_BLACKBIRD not in hass.data: + hass.data[DATA_BLACKBIRD] = {} + + port = config.get(CONF_PORT) + host = config.get(CONF_HOST) + + connection = None + if port is not None: + try: + blackbird = get_blackbird(port) + connection = port + except SerialException: + _LOGGER.error("Error connecting to the Blackbird controller") + return + + if host is not None: + try: + blackbird = get_blackbird(host, False) + connection = host + except socket.timeout: + _LOGGER.error("Error connecting to the Blackbird controller") + return + + sources = { + source_id: extra[CONF_NAME] for source_id, extra in config[CONF_SOURCES].items() + } + + devices = [] + for zone_id, extra in config[CONF_ZONES].items(): + _LOGGER.info("Adding zone %d - %s", zone_id, extra[CONF_NAME]) + unique_id = f"{connection}-{zone_id}" + device = BlackbirdZone(blackbird, sources, zone_id, extra[CONF_NAME]) + hass.data[DATA_BLACKBIRD][unique_id] = device + devices.append(device) + + add_entities(devices, True) + + def service_handle(service): + """Handle for services.""" + entity_ids = service.data.get(ATTR_ENTITY_ID) + source = service.data.get(ATTR_SOURCE) + if entity_ids: + devices = [ + device + for device in hass.data[DATA_BLACKBIRD].values() + if device.entity_id in entity_ids + ] + + else: + devices = hass.data[DATA_BLACKBIRD].values() + + for device in devices: + if service.service == SERVICE_SETALLZONES: + device.set_all_zones(source) + + hass.services.register( + DOMAIN, SERVICE_SETALLZONES, service_handle, schema=BLACKBIRD_SETALLZONES_SCHEMA + ) + + +class BlackbirdZone(MediaPlayerEntity): + """Representation of a Blackbird matrix zone.""" + + def __init__(self, blackbird, sources, zone_id, zone_name): + """Initialize new zone.""" + self._blackbird = blackbird + # dict source_id -> source name + self._source_id_name = sources + # dict source name -> source_id + self._source_name_id = {v: k for k, v in sources.items()} + # ordered list of all source names + self._source_names = sorted( + self._source_name_id.keys(), key=lambda v: self._source_name_id[v] + ) + self._zone_id = zone_id + self._name = zone_name + self._state = None + self._source = None + + def update(self): + """Retrieve latest state.""" + state = self._blackbird.zone_status(self._zone_id) + if not state: + return + self._state = STATE_ON if state.power else STATE_OFF + idx = state.av + if idx in self._source_id_name: + self._source = self._source_id_name[idx] + else: + self._source = None + + @property + def name(self): + """Return the name of the zone.""" + return self._name + + @property + def state(self): + """Return the state of the zone.""" + return self._state + + @property + def supported_features(self): + """Return flag of media commands that are supported.""" + return SUPPORT_BLACKBIRD + + @property + def media_title(self): + """Return the current source as media title.""" + return self._source + + @property + def source(self): + """Return the current input source of the device.""" + return self._source + + @property + def source_list(self): + """List of available input sources.""" + return self._source_names + + def set_all_zones(self, source): + """Set all zones to one source.""" + if source not in self._source_name_id: + return + idx = self._source_name_id[source] + _LOGGER.debug("Setting all zones source to %s", idx) + self._blackbird.set_all_zone_source(idx) + + def select_source(self, source): + """Set input source.""" + if source not in self._source_name_id: + return + idx = self._source_name_id[source] + _LOGGER.debug("Setting zone %d source to %s", self._zone_id, idx) + self._blackbird.set_zone_source(self._zone_id, idx) + + def turn_on(self): + """Turn the media player on.""" + _LOGGER.debug("Turning zone %d on", self._zone_id) + self._blackbird.set_zone_power(self._zone_id, True) + + def turn_off(self): + """Turn the media player off.""" + _LOGGER.debug("Turning zone %d off", self._zone_id) + self._blackbird.set_zone_power(self._zone_id, False) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blackbird/services.yaml b/homeassistant-2021.6.0.dev0/homeassistant/components/blackbird/services.yaml new file mode 100644 index 00000000000..7b3096c25e4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blackbird/services.yaml @@ -0,0 +1,20 @@ +set_all_zones: + name: Set all zones + description: Set all Blackbird zones to a single source. + fields: + entity_id: + name: Entity + description: Name of any blackbird zone. + required: true + example: "media_player.zone_1" + selector: + entity: + integration: blackbird + domain: media_player + source: + name: Source + description: Name of source to switch to. + required: true + example: "Source 1" + selector: + text: diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/__init__.py b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/__init__.py new file mode 100644 index 00000000000..fe2265ed78d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/__init__.py @@ -0,0 +1,110 @@ +"""The BleBox devices integration.""" +import logging + +from blebox_uniapi.error import Error +from blebox_uniapi.products import Products +from blebox_uniapi.session import ApiHost + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import Entity + +from .const import DEFAULT_SETUP_TIMEOUT, DOMAIN, PRODUCT + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["cover", "sensor", "switch", "air_quality", "light", "climate"] + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up BleBox devices from a config entry.""" + + websession = async_get_clientsession(hass) + + host = entry.data[CONF_HOST] + port = entry.data[CONF_PORT] + timeout = DEFAULT_SETUP_TIMEOUT + + api_host = ApiHost(host, port, timeout, websession, hass.loop) + + try: + product = await Products.async_from_host(api_host) + except Error as ex: + _LOGGER.error("Identify failed at %s:%d (%s)", api_host.host, api_host.port, ex) + raise ConfigEntryNotReady from ex + + domain = hass.data.setdefault(DOMAIN, {}) + domain_entry = domain.setdefault(entry.entry_id, {}) + product = domain_entry.setdefault(PRODUCT, product) + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """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 + + +@callback +def create_blebox_entities( + hass, config_entry, async_add_entities, entity_klass, entity_type +): + """Create entities from a BleBox product's features.""" + + product = hass.data[DOMAIN][config_entry.entry_id][PRODUCT] + + entities = [] + if entity_type in product.features: + for feature in product.features[entity_type]: + entities.append(entity_klass(feature)) + + async_add_entities(entities, True) + + +class BleBoxEntity(Entity): + """Implements a common class for entities representing a BleBox feature.""" + + def __init__(self, feature): + """Initialize a BleBox entity.""" + self._feature = feature + + @property + def name(self): + """Return the internal entity name.""" + return self._feature.full_name + + @property + def unique_id(self): + """Return a unique id.""" + return self._feature.unique_id + + async def async_update(self): + """Update the entity state.""" + try: + await self._feature.async_update() + except Error as ex: + _LOGGER.error("Updating '%s' failed: %s", self.name, ex) + + @property + def device_info(self): + """Return device information for this entity.""" + product = self._feature.product + return { + "identifiers": {(DOMAIN, product.unique_id)}, + "name": product.name, + "manufacturer": product.brand, + "model": product.model, + "sw_version": product.firmware_version, + } diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/air_quality.py b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/air_quality.py new file mode 100644 index 00000000000..e7e9bac1f97 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/air_quality.py @@ -0,0 +1,36 @@ +"""BleBox air quality entity.""" + +from homeassistant.components.air_quality import AirQualityEntity + +from . import BleBoxEntity, create_blebox_entities + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a BleBox air quality entity.""" + create_blebox_entities( + hass, config_entry, async_add_entities, BleBoxAirQualityEntity, "air_qualities" + ) + + +class BleBoxAirQualityEntity(BleBoxEntity, AirQualityEntity): + """Representation of a BleBox air quality feature.""" + + @property + def icon(self): + """Return the icon.""" + return "mdi:blur" + + @property + def particulate_matter_0_1(self): + """Return the particulate matter 0.1 level.""" + return self._feature.pm1 + + @property + def particulate_matter_2_5(self): + """Return the particulate matter 2.5 level.""" + return self._feature.pm2_5 + + @property + def particulate_matter_10(self): + """Return the particulate matter 10 level.""" + return self._feature.pm10 diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/climate.py b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/climate.py new file mode 100644 index 00000000000..4ee8cf9be76 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/climate.py @@ -0,0 +1,92 @@ +"""BleBox climate entity.""" + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS + +from . import BleBoxEntity, create_blebox_entities + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a BleBox climate entity.""" + + create_blebox_entities( + hass, config_entry, async_add_entities, BleBoxClimateEntity, "climates" + ) + + +class BleBoxClimateEntity(BleBoxEntity, ClimateEntity): + """Representation of a BleBox climate feature (saunaBox).""" + + @property + def supported_features(self): + """Return the supported climate features.""" + return SUPPORT_TARGET_TEMPERATURE + + @property + def hvac_mode(self): + """Return the desired HVAC mode.""" + if self._feature.is_on is None: + return None + + return HVAC_MODE_HEAT if self._feature.is_on else HVAC_MODE_OFF + + @property + def hvac_action(self): + """Return the actual current HVAC action.""" + is_on = self._feature.is_on + if not is_on: + return None if is_on is None else CURRENT_HVAC_OFF + + # NOTE: In practice, there's no need to handle case when is_heating is None + return CURRENT_HVAC_HEAT if self._feature.is_heating else CURRENT_HVAC_IDLE + + @property + def hvac_modes(self): + """Return a list of possible HVAC modes.""" + return [HVAC_MODE_OFF, HVAC_MODE_HEAT] + + @property + def temperature_unit(self): + """Return the temperature unit.""" + return TEMP_CELSIUS + + @property + def max_temp(self): + """Return the maximum temperature supported.""" + return self._feature.max_temp + + @property + def min_temp(self): + """Return the maximum temperature supported.""" + return self._feature.min_temp + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._feature.current + + @property + def target_temperature(self): + """Return the desired thermostat temperature.""" + return self._feature.desired + + async def async_set_hvac_mode(self, hvac_mode): + """Set the climate entity mode.""" + if hvac_mode == HVAC_MODE_HEAT: + await self._feature.async_on() + return + + await self._feature.async_off() + + async def async_set_temperature(self, **kwargs): + """Set the thermostat temperature.""" + value = kwargs[ATTR_TEMPERATURE] + await self._feature.async_set_temperature(value) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/config_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/config_flow.py new file mode 100644 index 00000000000..17dffe154d1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/config_flow.py @@ -0,0 +1,127 @@ +"""Config flow for BleBox devices integration.""" +import logging + +from blebox_uniapi.error import Error, UnsupportedBoxVersion +from blebox_uniapi.products import Products +from blebox_uniapi.session import ApiHost +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import ( + ADDRESS_ALREADY_CONFIGURED, + CANNOT_CONNECT, + DEFAULT_HOST, + DEFAULT_PORT, + DEFAULT_SETUP_TIMEOUT, + DOMAIN, + UNKNOWN, + UNSUPPORTED_VERSION, +) + +_LOGGER = logging.getLogger(__name__) + + +def host_port(data): + """Return a list with host and port.""" + return (data[CONF_HOST], data[CONF_PORT]) + + +def create_schema(previous_input=None): + """Create a schema with given values as default.""" + if previous_input is not None: + host, port = host_port(previous_input) + else: + host = DEFAULT_HOST + port = DEFAULT_PORT + + return vol.Schema( + { + vol.Required(CONF_HOST, default=host): str, + vol.Required(CONF_PORT, default=port): int, + } + ) + + +LOG_MSG = { + UNSUPPORTED_VERSION: "Outdated firmware", + CANNOT_CONNECT: "Failed to identify device", + UNKNOWN: "Unknown error while identifying device", +} + + +class BleBoxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for BleBox devices.""" + + VERSION = 1 + + def __init__(self): + """Initialize the BleBox config flow.""" + self.device_config = {} + + def handle_step_exception( + self, step, exception, schema, host, port, message_id, log_fn + ): + """Handle step exceptions.""" + + log_fn("%s at %s:%d (%s)", LOG_MSG[message_id], host, port, exception) + + return self.async_show_form( + step_id="user", + data_schema=schema, + errors={"base": message_id}, + description_placeholders={"address": f"{host}:{port}"}, + ) + + async def async_step_user(self, user_input=None): + """Handle initial user-triggered config step.""" + + hass = self.hass + schema = create_schema(user_input) + + if user_input is None: + return self.async_show_form( + step_id="user", + data_schema=schema, + errors={}, + description_placeholders={}, + ) + + addr = host_port(user_input) + + for entry in self._async_current_entries(): + if addr == host_port(entry.data): + host, port = addr + return self.async_abort( + reason=ADDRESS_ALREADY_CONFIGURED, + description_placeholders={"address": f"{host}:{port}"}, + ) + + websession = async_get_clientsession(hass) + api_host = ApiHost(*addr, DEFAULT_SETUP_TIMEOUT, websession, hass.loop, _LOGGER) + + try: + product = await Products.async_from_host(api_host) + + except UnsupportedBoxVersion as ex: + return self.handle_step_exception( + "user", ex, schema, *addr, UNSUPPORTED_VERSION, _LOGGER.debug + ) + + except Error as ex: + return self.handle_step_exception( + "user", ex, schema, *addr, CANNOT_CONNECT, _LOGGER.warning + ) + + except RuntimeError as ex: + return self.handle_step_exception( + "user", ex, schema, *addr, UNKNOWN, _LOGGER.error + ) + + # Check if configured but IP changed since + await self.async_set_unique_id(product.unique_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=product.name, data=user_input) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/const.py b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/const.py new file mode 100644 index 00000000000..f5eba403c75 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/const.py @@ -0,0 +1,51 @@ +"""Constants for the BleBox devices integration.""" + +from homeassistant.components.cover import ( + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GATE, + DEVICE_CLASS_SHUTTER, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.components.switch import DEVICE_CLASS_SWITCH +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS + +DOMAIN = "blebox" +PRODUCT = "product" + +DEFAULT_SETUP_TIMEOUT = 3 + +# translation strings +ADDRESS_ALREADY_CONFIGURED = "address_already_configured" +CANNOT_CONNECT = "cannot_connect" +UNSUPPORTED_VERSION = "unsupported_version" +UNKNOWN = "unknown" + +BLEBOX_TO_HASS_DEVICE_CLASSES = { + "shutter": DEVICE_CLASS_SHUTTER, + "gatebox": DEVICE_CLASS_DOOR, + "gate": DEVICE_CLASS_GATE, + "relay": DEVICE_CLASS_SWITCH, + "temperature": DEVICE_CLASS_TEMPERATURE, +} + +BLEBOX_TO_HASS_COVER_STATES = { + None: None, + 0: STATE_CLOSING, # moving down + 1: STATE_OPENING, # moving up + 2: STATE_OPEN, # manually stopped + 3: STATE_CLOSED, # lower limit + 4: STATE_OPEN, # upper limit / open + # gateController + 5: STATE_OPEN, # overload + 6: STATE_OPEN, # motor failure + # 7 is not used + 8: STATE_OPEN, # safety stop +} + +BLEBOX_TO_UNIT_MAP = {"celsius": TEMP_CELSIUS} + +DEFAULT_HOST = "192.168.0.2" +DEFAULT_PORT = 80 diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/cover.py b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/cover.py new file mode 100644 index 00000000000..620adacf3f6 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/cover.py @@ -0,0 +1,92 @@ +"""BleBox cover entity.""" + +from homeassistant.components.cover import ( + ATTR_POSITION, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPENING, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + SUPPORT_STOP, + CoverEntity, +) + +from . import BleBoxEntity, create_blebox_entities +from .const import BLEBOX_TO_HASS_COVER_STATES, BLEBOX_TO_HASS_DEVICE_CLASSES + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a BleBox entry.""" + + create_blebox_entities( + hass, config_entry, async_add_entities, BleBoxCoverEntity, "covers" + ) + + +class BleBoxCoverEntity(BleBoxEntity, CoverEntity): + """Representation of a BleBox cover feature.""" + + @property + def state(self): + """Return the equivalent HA cover state.""" + return BLEBOX_TO_HASS_COVER_STATES[self._feature.state] + + @property + def device_class(self): + """Return the device class.""" + return BLEBOX_TO_HASS_DEVICE_CLASSES[self._feature.device_class] + + @property + def supported_features(self): + """Return the supported cover features.""" + position = SUPPORT_SET_POSITION if self._feature.is_slider else 0 + stop = SUPPORT_STOP if self._feature.has_stop else 0 + + return position | stop | SUPPORT_OPEN | SUPPORT_CLOSE + + @property + def current_cover_position(self): + """Return the current cover position.""" + position = self._feature.current + if position == -1: # possible for shutterBox + return None + + return None if position is None else 100 - position + + @property + def is_opening(self): + """Return whether cover is opening.""" + return self._is_state(STATE_OPENING) + + @property + def is_closing(self): + """Return whether cover is closing.""" + return self._is_state(STATE_CLOSING) + + @property + def is_closed(self): + """Return whether cover is closed.""" + return self._is_state(STATE_CLOSED) + + async def async_open_cover(self, **kwargs): + """Open the cover position.""" + await self._feature.async_open() + + async def async_close_cover(self, **kwargs): + """Close the cover position.""" + await self._feature.async_close() + + async def async_set_cover_position(self, **kwargs): + """Set the cover position.""" + + position = kwargs[ATTR_POSITION] + await self._feature.async_set_position(100 - position) + + async def async_stop_cover(self, **kwargs): + """Stop the cover.""" + await self._feature.async_stop() + + def _is_state(self, state_name): + value = self.state + return None if value is None else value == state_name diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/light.py b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/light.py new file mode 100644 index 00000000000..a825d102717 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/light.py @@ -0,0 +1,100 @@ +"""BleBox light entities implementation.""" +import logging + +from blebox_uniapi.error import BadOnValueError + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + ATTR_WHITE_VALUE, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_WHITE_VALUE, + LightEntity, +) +from homeassistant.util.color import ( + color_hs_to_RGB, + color_rgb_to_hex, + color_RGB_to_hs, + rgb_hex_to_rgb_list, +) + +from . import BleBoxEntity, create_blebox_entities + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a BleBox entry.""" + + create_blebox_entities( + hass, config_entry, async_add_entities, BleBoxLightEntity, "lights" + ) + + +class BleBoxLightEntity(BleBoxEntity, LightEntity): + """Representation of BleBox lights.""" + + @property + def supported_features(self): + """Return supported features.""" + white = SUPPORT_WHITE_VALUE if self._feature.supports_white else 0 + color = SUPPORT_COLOR if self._feature.supports_color else 0 + brightness = SUPPORT_BRIGHTNESS if self._feature.supports_brightness else 0 + return white | color | brightness + + @property + def is_on(self): + """Return if light is on.""" + return self._feature.is_on + + @property + def brightness(self): + """Return the name.""" + return self._feature.brightness + + @property + def white_value(self): + """Return the white value.""" + return self._feature.white_value + + @property + def hs_color(self): + """Return the hue and saturation.""" + rgbw_hex = self._feature.rgbw_hex + if rgbw_hex is None: + return None + + rgb = rgb_hex_to_rgb_list(rgbw_hex)[0:3] + return color_RGB_to_hs(*rgb) + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + + white = kwargs.get(ATTR_WHITE_VALUE) + hs_color = kwargs.get(ATTR_HS_COLOR) + brightness = kwargs.get(ATTR_BRIGHTNESS) + + feature = self._feature + value = feature.sensible_on_value + + if brightness is not None: + value = feature.apply_brightness(value, brightness) + + if white is not None: + value = feature.apply_white(value, white) + + if hs_color is not None: + raw_rgb = color_rgb_to_hex(*color_hs_to_RGB(*hs_color)) + value = feature.apply_color(value, raw_rgb) + + try: + await self._feature.async_on(value) + except BadOnValueError as ex: + _LOGGER.error( + "Turning on '%s' failed: Bad value %s (%s)", self.name, value, ex + ) + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + await self._feature.async_off() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/manifest.json b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/manifest.json new file mode 100644 index 00000000000..00b4b61c507 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "blebox", + "name": "BleBox devices", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/blebox", + "requirements": ["blebox_uniapi==1.3.2"], + "codeowners": ["@gadgetmobile"], + "iot_class": "local_polling" +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/sensor.py b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/sensor.py new file mode 100644 index 00000000000..c1b9d8501c1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/sensor.py @@ -0,0 +1,33 @@ +"""BleBox sensor entities.""" + +from homeassistant.components.sensor import SensorEntity + +from . import BleBoxEntity, create_blebox_entities +from .const import BLEBOX_TO_HASS_DEVICE_CLASSES, BLEBOX_TO_UNIT_MAP + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a BleBox entry.""" + + create_blebox_entities( + hass, config_entry, async_add_entities, BleBoxSensorEntity, "sensors" + ) + + +class BleBoxSensorEntity(BleBoxEntity, SensorEntity): + """Representation of a BleBox sensor feature.""" + + @property + def state(self): + """Return the state.""" + return self._feature.current + + @property + def unit_of_measurement(self): + """Return the unit.""" + return BLEBOX_TO_UNIT_MAP[self._feature.unit] + + @property + def device_class(self): + """Return the device class.""" + return BLEBOX_TO_HASS_DEVICE_CLASSES[self._feature.device_class] diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/strings.json new file mode 100644 index 00000000000..b179f0d097b --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/strings.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "address_already_configured": "A BleBox device is already configured at {address}." + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unsupported_version": "BleBox device has outdated firmware. Please upgrade it first.", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "description": "Set up your BleBox to integrate with Home Assistant.", + "data": { + "host": "[%key:common::config_flow::data::ip%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "title": "Set up your BleBox device" + } + } + } +} diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/switch.py b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/switch.py new file mode 100644 index 00000000000..e88773db639 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/switch.py @@ -0,0 +1,34 @@ +"""BleBox switch implementation.""" +from homeassistant.components.switch import SwitchEntity + +from . import BleBoxEntity, create_blebox_entities +from .const import BLEBOX_TO_HASS_DEVICE_CLASSES + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a BleBox switch entity.""" + create_blebox_entities( + hass, config_entry, async_add_entities, BleBoxSwitchEntity, "switches" + ) + + +class BleBoxSwitchEntity(BleBoxEntity, SwitchEntity): + """Representation of a BleBox switch feature.""" + + @property + def device_class(self): + """Return the device class.""" + return BLEBOX_TO_HASS_DEVICE_CLASSES[self._feature.device_class] + + @property + def is_on(self): + """Return whether switch is on.""" + return self._feature.is_on + + async def async_turn_on(self, **kwargs): + """Turn on the switch.""" + await self._feature.async_turn_on() + + async def async_turn_off(self, **kwargs): + """Turn off the switch.""" + await self._feature.async_turn_off() diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/ca.json b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/ca.json new file mode 100644 index 00000000000..d2b25c7590a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/ca.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "address_already_configured": "Ja hi ha un dispositiu BleBox configurat a {address}.", + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat", + "unsupported_version": "El dispositiu BleBox t\u00e9 un firmware obsolet. Primer actualitza'l." + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "Adre\u00e7a IP", + "port": "Port" + }, + "description": "Configura el teu dispositiu BleBox per a integrar-lo a Home Assistant.", + "title": "Configuraci\u00f3 del dispositiu BleBox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/cs.json b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/cs.json new file mode 100644 index 00000000000..ad9e6007ac7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/cs.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "address_already_configured": "Za\u0159\u00edzen\u00ed BleBox je ji\u017e na adrese {address} nastaveno.", + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba", + "unsupported_version": "Za\u0159\u00edzen\u00ed BleBox m\u00e1 zastaral\u00fd firmware. Nejprve jej upgradujte." + }, + "flow_title": "Za\u0159\u00edzen\u00ed BleBox: {name} ({host})", + "step": { + "user": { + "data": { + "host": "IP adresa", + "port": "Port" + }, + "description": "Nastavte BleBox k integraci s Home Assistant.", + "title": "Nastaven\u00ed za\u0159\u00edzen\u00ed BleBox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/de.json b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/de.json new file mode 100644 index 00000000000..37c8dde54e5 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/de.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "address_already_configured": "Ein BleBox-Ger\u00e4t ist bereits unter {address} konfiguriert.", + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler", + "unsupported_version": "Das BleBox-Ger\u00e4t hat eine veraltete Firmware. Bitte aktualisieren Sie es zuerst." + }, + "flow_title": "BleBox-Ger\u00e4t: {name} ( {host} )", + "step": { + "user": { + "data": { + "host": "IP Adresse", + "port": "Port" + }, + "description": "Richten Sie Ihre BleBox f\u00fcr die Integration mit dem Home Assistant ein.", + "title": "Richten Sie Ihr BleBox-Ger\u00e4t ein" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/en.json b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/en.json new file mode 100644 index 00000000000..d6f9f11498a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "address_already_configured": "A BleBox device is already configured at {address}.", + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error", + "unsupported_version": "BleBox device has outdated firmware. Please upgrade it first." + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "IP Address", + "port": "Port" + }, + "description": "Set up your BleBox to integrate with Home Assistant.", + "title": "Set up your BleBox device" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/es-419.json b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/es-419.json new file mode 100644 index 00000000000..eb0545e4fa4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/es-419.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_already_configured": "Un dispositivo BleBox ya est\u00e1 configurado en {address} .", + "already_configured": "Este dispositivo BleBox ya est\u00e1 configurado." + }, + "error": { + "cannot_connect": "No se puede conectar al dispositivo BleBox. (Verifique los registros en busca de errores).", + "unknown": "Error desconocido al conectarse al dispositivo BleBox. (Verifique los registros en busca de errores).", + "unsupported_version": "El dispositivo BleBox tiene un firmware desactualizado. Por favor, actual\u00edcelo primero." + }, + "flow_title": "Dispositivo BleBox: {name} ({host})", + "step": { + "user": { + "data": { + "host": "Direcci\u00f3n IP", + "port": "Puerto" + }, + "description": "Configure su BleBox para integrarse con Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/es.json b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/es.json new file mode 100644 index 00000000000..a415edd9809 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/es.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "address_already_configured": "Un dispositivo BleBox ya est\u00e1 configurado en {address}.", + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado", + "unsupported_version": "El dispositivo BleBox tiene un firmware anticuado. Por favor, actual\u00edzalo primero." + }, + "flow_title": "Dispositivo BleBox: {name} ({host})", + "step": { + "user": { + "data": { + "host": "Direcci\u00f3n IP", + "port": "Puerto" + }, + "description": "Configura tu BleBox para integrarse con Home Assistant.", + "title": "Configura tu dispositivo BleBox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/et.json b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/et.json new file mode 100644 index 00000000000..913428a897e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/et.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "address_already_configured": "BleBox seade {address} on juba m\u00e4\u00e4ratud", + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Tundmatu viga", + "unsupported_version": "BleBoxi seadmel on vananenud p\u00fcsivara. Esmalt v\u00e4rskenda seda." + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "IP aadress", + "port": "" + }, + "description": "Seadista oma BleBox sidumine Home Assistant-iga.", + "title": "Seadista BleBox seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/fi.json b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/fi.json new file mode 100644 index 00000000000..d99fac1fdf3 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/fi.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Portti" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/fr.json b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/fr.json new file mode 100644 index 00000000000..d30d026d177 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "address_already_configured": "Un p\u00e9riph\u00e9rique BleBox est d\u00e9j\u00e0 configur\u00e9 \u00e0 {address}.", + "already_configured": "Ce p\u00e9riph\u00e9rique BleBox est d\u00e9j\u00e0 configur\u00e9." + }, + "error": { + "cannot_connect": "Impossible de connecter le p\u00e9riph\u00e9rique BleBox. (V\u00e9rifiez les journaux pour les erreurs.)", + "unknown": "Erreur inconnue lors de la connexion au p\u00e9riph\u00e9rique BleBox. (V\u00e9rifiez les journaux pour les erreurs.)", + "unsupported_version": "L'appareil BleBox a un micrologiciel obsol\u00e8te. Veuillez d'abord le mettre \u00e0 jour." + }, + "flow_title": "P\u00e9riph\u00e9rique Blebox: {name} ({host)}", + "step": { + "user": { + "data": { + "host": "Adresse IP", + "port": "Port" + }, + "description": "Configurez votre BleBox pour l'int\u00e9grer \u00e0 Home Assistant.", + "title": "Configurer votre appareil BleBox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/he.json b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/he.json new file mode 100644 index 00000000000..001f8457f14 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", + "port": "\u05e4\u05d5\u05e8\u05d8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/hu.json b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/hu.json new file mode 100644 index 00000000000..9649d70d976 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "unsupported_version": "A BleBox eszk\u00f6z elavult firmware-rel rendelkezik. El\u0151sz\u00f6r friss\u00edtsd." + }, + "flow_title": "BleBox eszk\u00f6z: {name} ({host})", + "step": { + "user": { + "data": { + "host": "IP c\u00edm", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/id.json b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/id.json new file mode 100644 index 00000000000..2ef604d1bff --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/id.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "address_already_configured": "Perangkat BleBox sudah dikonfigurasi di {address}.", + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan", + "unsupported_version": "Firmware Perangkat BleBox sudah usang. Tingkatkan terlebih dulu." + }, + "flow_title": "Perangkat BleBox: {name} ({host})", + "step": { + "user": { + "data": { + "host": "Alamat IP", + "port": "Port" + }, + "description": "Siapkan BleBox Anda untuk diintegrasikan dengan Home Assistant.", + "title": "Siapkan perangkat BleBox Anda" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/it.json b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/it.json new file mode 100644 index 00000000000..6d377840e90 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/it.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "address_already_configured": "Un dispositivo BleBox \u00e8 gi\u00e0 configurato in {address}.", + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto", + "unsupported_version": "Il dispositivo BleBox ha un firmware obsoleto. Si prega di aggiornarlo prima." + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "Indirizzo IP", + "port": "Porta" + }, + "description": "Configura BleBox per l'integrazione con Home Assistant.", + "title": "Configura il tuo dispositivo BleBox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/ko.json b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/ko.json new file mode 100644 index 00000000000..1032e873ae9 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/ko.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "address_already_configured": "BleBox \uae30\uae30\uac00 {address}(\uc73c)\ub85c \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", + "unsupported_version": "BleBox \uae30\uae30 \ud38c\uc6e8\uc5b4\uac00 \uc624\ub798\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uba3c\uc800 \uc5c5\uadf8\ub808\uc774\ub4dc\ud574\uc8fc\uc138\uc694." + }, + "flow_title": "BleBox \uae30\uae30: {name} ({host})", + "step": { + "user": { + "data": { + "host": "IP \uc8fc\uc18c", + "port": "\ud3ec\ud2b8" + }, + "description": "Home Assistant\uc5d0 BleBox \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4.", + "title": "BleBox \uae30\uae30 \uc124\uc815\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/lb.json b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/lb.json new file mode 100644 index 00000000000..19602c9f6fd --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/lb.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "address_already_configured": "Ee BleBox Apparat ass scho konfigur\u00e9iert op {adress}.", + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "unknown": "Onerwaarte Feeler", + "unsupported_version": "BleBox Apparat huet eng aal Firmware. Maach fir d'\u00e9ischt d'Mise \u00e0 jour." + }, + "flow_title": "BleBox Apparat: {name} ({host})", + "step": { + "user": { + "data": { + "host": "IP Adresse", + "port": "Port" + }, + "description": "BleBox ariichten fir d'Integratioun mam Home Assistant.", + "title": "BleBox Apparat ariichten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/nl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/nl.json new file mode 100644 index 00000000000..65a775e6f72 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "address_already_configured": "Er is al een BleBox-apparaat geconfigureerd op {address} .", + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kon niet verbinden", + "unknown": "Onverwachte fout", + "unsupported_version": "BleBox-apparaat heeft verouderde firmware. Upgrade het eerst." + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "IP-adres", + "port": "Poort" + }, + "description": "Stel uw BleBox in om te integreren met Home Assistant.", + "title": "Stel uw BleBox-apparaat in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/no.json b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/no.json new file mode 100644 index 00000000000..3ab5987eba7 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/no.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "address_already_configured": "En BleBox-enhet er allerede konfigurert p\u00e5 {address} .", + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil", + "unsupported_version": "BleBox-enheten har utdatert fastvare. Vennligst oppgrader den f\u00f8rst." + }, + "flow_title": "{name} ( {host} )", + "step": { + "user": { + "data": { + "host": "IP adresse", + "port": "Port" + }, + "description": "Konfigurer BleBox-en til \u00e5 integreres med Home Assistant.", + "title": "Konfigurere BleBox-enheten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/pl.json b/homeassistant-2021.6.0.dev0/homeassistant/components/blebox/translations/pl.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant-2021.6.0.dev0/homeassistant/config.py b/homeassistant-2021.6.0.dev0/homeassistant/config.py new file mode 100644 index 00000000000..c5f29f3c3c1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/config.py @@ -0,0 +1,939 @@ +"""Module to help with parsing and generating configuration files.""" +from __future__ import annotations + +from collections import OrderedDict +from collections.abc import Sequence +import logging +import os +from pathlib import Path +import re +import shutil +from types import ModuleType +from typing import Any, Callable + +from awesomeversion import AwesomeVersion +import voluptuous as vol +from voluptuous.humanize import humanize_error + +from homeassistant import auth +from homeassistant.auth import ( + mfa_modules as auth_mfa_modules, + providers as auth_providers, +) +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_FRIENDLY_NAME, + ATTR_HIDDEN, + CONF_ALLOWLIST_EXTERNAL_DIRS, + CONF_ALLOWLIST_EXTERNAL_URLS, + CONF_AUTH_MFA_MODULES, + CONF_AUTH_PROVIDERS, + CONF_CUSTOMIZE, + CONF_CUSTOMIZE_DOMAIN, + CONF_CUSTOMIZE_GLOB, + CONF_ELEVATION, + CONF_EXTERNAL_URL, + CONF_ID, + CONF_INTERNAL_URL, + CONF_LATITUDE, + CONF_LEGACY_TEMPLATES, + CONF_LONGITUDE, + CONF_MEDIA_DIRS, + CONF_NAME, + CONF_PACKAGES, + CONF_TEMPERATURE_UNIT, + CONF_TIME_ZONE, + CONF_TYPE, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + LEGACY_CONF_WHITELIST_EXTERNAL_DIRS, + TEMP_CELSIUS, + __version__, +) +from homeassistant.core import DOMAIN as CONF_CORE, SOURCE_YAML, HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_per_platform, extract_domain_configs +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_values import EntityValues +from homeassistant.helpers.typing import ConfigType +from homeassistant.loader import Integration, IntegrationNotFound +from homeassistant.requirements import ( + RequirementsNotFound, + async_get_integration_with_requirements, +) +from homeassistant.util.package import is_docker_env +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM +from homeassistant.util.yaml import SECRET_YAML, Secrets, load_yaml + +_LOGGER = logging.getLogger(__name__) + +DATA_PERSISTENT_ERRORS = "bootstrap_persistent_errors" +RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml") +RE_ASCII = re.compile(r"\033\[[^m]*m") +YAML_CONFIG_FILE = "configuration.yaml" +VERSION_FILE = ".HA_VERSION" +CONFIG_DIR_NAME = ".homeassistant" +DATA_CUSTOMIZE = "hass_customize" + +GROUP_CONFIG_PATH = "groups.yaml" +AUTOMATION_CONFIG_PATH = "automations.yaml" +SCRIPT_CONFIG_PATH = "scripts.yaml" +SCENE_CONFIG_PATH = "scenes.yaml" + +LOAD_EXCEPTIONS = (ImportError, FileNotFoundError) +INTEGRATION_LOAD_EXCEPTIONS = ( + IntegrationNotFound, + RequirementsNotFound, + *LOAD_EXCEPTIONS, +) + +DEFAULT_CONFIG = f""" +# Configure a default setup of Home Assistant (frontend, api, etc) +default_config: + +# Text to speech +tts: + - platform: google_translate + +group: !include {GROUP_CONFIG_PATH} +automation: !include {AUTOMATION_CONFIG_PATH} +script: !include {SCRIPT_CONFIG_PATH} +scene: !include {SCENE_CONFIG_PATH} +""" +DEFAULT_SECRETS = """ +# Use this file to store secrets like usernames and passwords. +# Learn more at https://www.home-assistant.io/docs/configuration/secrets/ +some_password: welcome +""" +TTS_PRE_92 = """ +tts: + - platform: google +""" +TTS_92 = """ +tts: + - platform: google_translate + service_name: google_say +""" + + +def _no_duplicate_auth_provider( + configs: Sequence[dict[str, Any]] +) -> Sequence[dict[str, Any]]: + """No duplicate auth provider config allowed in a list. + + Each type of auth provider can only have one config without optional id. + Unique id is required if same type of auth provider used multiple times. + """ + config_keys: set[tuple[str, str | None]] = set() + for config in configs: + key = (config[CONF_TYPE], config.get(CONF_ID)) + if key in config_keys: + raise vol.Invalid( + f"Duplicate auth provider {config[CONF_TYPE]} found. " + "Please add unique IDs " + "if you want to have the same auth provider twice" + ) + config_keys.add(key) + return configs + + +def _no_duplicate_auth_mfa_module( + configs: Sequence[dict[str, Any]] +) -> Sequence[dict[str, Any]]: + """No duplicate auth mfa module item allowed in a list. + + Each type of mfa module can only have one config without optional id. + A global unique id is required if same type of mfa module used multiple + times. + Note: this is different than auth provider + """ + config_keys: set[str] = set() + for config in configs: + key = config.get(CONF_ID, config[CONF_TYPE]) + if key in config_keys: + raise vol.Invalid( + f"Duplicate mfa module {config[CONF_TYPE]} found. " + "Please add unique IDs " + "if you want to have the same mfa module twice" + ) + config_keys.add(key) + return configs + + +PACKAGES_CONFIG_SCHEMA = cv.schema_with_slug_keys( # Package names are slugs + vol.Schema({cv.string: vol.Any(dict, list, None)}) # Component config +) + +CUSTOMIZE_DICT_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_FRIENDLY_NAME): cv.string, + vol.Optional(ATTR_HIDDEN): cv.boolean, + vol.Optional(ATTR_ASSUMED_STATE): cv.boolean, + }, + extra=vol.ALLOW_EXTRA, +) + +CUSTOMIZE_CONFIG_SCHEMA = vol.Schema( + { + vol.Optional(CONF_CUSTOMIZE, default={}): vol.Schema( + {cv.entity_id: CUSTOMIZE_DICT_SCHEMA} + ), + vol.Optional(CONF_CUSTOMIZE_DOMAIN, default={}): vol.Schema( + {cv.string: CUSTOMIZE_DICT_SCHEMA} + ), + vol.Optional(CONF_CUSTOMIZE_GLOB, default={}): vol.Schema( + {cv.string: CUSTOMIZE_DICT_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, + } +) + + +def get_default_config_dir() -> str: + """Put together the default configuration directory based on the OS.""" + data_dir = os.getenv("APPDATA") if os.name == "nt" else os.path.expanduser("~") + return os.path.join(data_dir, CONFIG_DIR_NAME) # type: ignore + + +async def async_ensure_config_exists(hass: HomeAssistant) -> bool: + """Ensure a configuration file exists in given configuration directory. + + Creating a default one if needed. + Return boolean if configuration dir is ready to go. + """ + config_path = hass.config.path(YAML_CONFIG_FILE) + + if os.path.isfile(config_path): + return True + + print( + "Unable to find configuration. Creating default one in", hass.config.config_dir + ) + return await async_create_default_config(hass) + + +async def async_create_default_config(hass: HomeAssistant) -> bool: + """Create a default configuration file in given configuration directory. + + Return if creation was successful. + """ + return await hass.async_add_executor_job( + _write_default_config, hass.config.config_dir + ) + + +def _write_default_config(config_dir: str) -> bool: + """Write the default config.""" + config_path = os.path.join(config_dir, YAML_CONFIG_FILE) + secret_path = os.path.join(config_dir, SECRET_YAML) + version_path = os.path.join(config_dir, VERSION_FILE) + group_yaml_path = os.path.join(config_dir, GROUP_CONFIG_PATH) + automation_yaml_path = os.path.join(config_dir, AUTOMATION_CONFIG_PATH) + script_yaml_path = os.path.join(config_dir, SCRIPT_CONFIG_PATH) + scene_yaml_path = os.path.join(config_dir, SCENE_CONFIG_PATH) + + # Writing files with YAML does not create the most human readable results + # So we're hard coding a YAML template. + try: + with open(config_path, "wt") as config_file: + config_file.write(DEFAULT_CONFIG) + + if not os.path.isfile(secret_path): + with open(secret_path, "wt") as secret_file: + secret_file.write(DEFAULT_SECRETS) + + with open(version_path, "wt") as version_file: + version_file.write(__version__) + + if not os.path.isfile(group_yaml_path): + with open(group_yaml_path, "wt"): + pass + + if not os.path.isfile(automation_yaml_path): + with open(automation_yaml_path, "wt") as automation_file: + automation_file.write("[]") + + if not os.path.isfile(script_yaml_path): + with open(script_yaml_path, "wt"): + pass + + if not os.path.isfile(scene_yaml_path): + with open(scene_yaml_path, "wt"): + pass + + return True + + except OSError: + print("Unable to create default configuration file", config_path) + return False + + +async def async_hass_config_yaml(hass: HomeAssistant) -> dict: + """Load YAML from a Home Assistant configuration file. + + This function allow a component inside the asyncio loop to reload its + configuration by itself. Include package merge. + """ + if hass.config.config_dir is None: + secrets = None + else: + secrets = Secrets(Path(hass.config.config_dir)) + + # Not using async_add_executor_job because this is an internal method. + config = await hass.loop.run_in_executor( + None, + load_yaml_config_file, + hass.config.path(YAML_CONFIG_FILE), + secrets, + ) + core_config = config.get(CONF_CORE, {}) + await merge_packages_config(hass, config, core_config.get(CONF_PACKAGES, {})) + return config + + +def load_yaml_config_file( + config_path: str, secrets: Secrets | None = None +) -> dict[Any, Any]: + """Parse a YAML configuration file. + + Raises FileNotFoundError or HomeAssistantError. + + This method needs to run in an executor. + """ + conf_dict = load_yaml(config_path, secrets) + + if not isinstance(conf_dict, dict): + msg = ( + f"The configuration file {os.path.basename(config_path)} " + "does not contain a dictionary" + ) + _LOGGER.error(msg) + raise HomeAssistantError(msg) + + # Convert values to dictionaries if they are None + for key, value in conf_dict.items(): + conf_dict[key] = value or {} + return conf_dict + + +def process_ha_config_upgrade(hass: HomeAssistant) -> None: + """Upgrade configuration if necessary. + + This method needs to run in an executor. + """ + version_path = hass.config.path(VERSION_FILE) + + try: + with open(version_path) as inp: + conf_version = inp.readline().strip() + except FileNotFoundError: + # Last version to not have this file + conf_version = "0.7.7" + + if conf_version == __version__: + return + + _LOGGER.info( + "Upgrading configuration directory from %s to %s", conf_version, __version__ + ) + + version_obj = AwesomeVersion(conf_version) + + if version_obj < AwesomeVersion("0.50"): + # 0.50 introduced persistent deps dir. + lib_path = hass.config.path("deps") + if os.path.isdir(lib_path): + shutil.rmtree(lib_path) + + if version_obj < AwesomeVersion("0.92"): + # 0.92 moved google/tts.py to google_translate/tts.py + config_path = hass.config.path(YAML_CONFIG_FILE) + + with open(config_path, encoding="utf-8") as config_file: + config_raw = config_file.read() + + if TTS_PRE_92 in config_raw: + _LOGGER.info("Migrating google tts to google_translate tts") + config_raw = config_raw.replace(TTS_PRE_92, TTS_92) + try: + with open(config_path, "wt", encoding="utf-8") as config_file: + config_file.write(config_raw) + except OSError: + _LOGGER.exception("Migrating to google_translate tts failed") + + if version_obj < AwesomeVersion("0.94") and is_docker_env(): + # In 0.94 we no longer install packages inside the deps folder when + # running inside a Docker container. + lib_path = hass.config.path("deps") + if os.path.isdir(lib_path): + shutil.rmtree(lib_path) + + with open(version_path, "wt") as outp: + outp.write(__version__) + + +@callback +def async_log_exception( + ex: Exception, + domain: str, + config: dict, + hass: HomeAssistant, + link: str | None = None, +) -> None: + """Log an error for configuration validation. + + This method must be run in the event loop. + """ + if hass is not None: + async_notify_setup_error(hass, domain, link) + message, is_friendly = _format_config_error(ex, domain, config, link) + _LOGGER.error(message, exc_info=not is_friendly and ex) + + +@callback +def _format_config_error( + ex: Exception, domain: str, config: dict, link: str | None = None +) -> tuple[str, bool]: + """Generate log exception for configuration validation. + + This method must be run in the event loop. + """ + is_friendly = False + message = f"Invalid config for [{domain}]: " + if isinstance(ex, vol.Invalid): + if "extra keys not allowed" in ex.error_message: + path = "->".join(str(m) for m in ex.path) + message += ( + f"[{ex.path[-1]}] is an invalid option for [{domain}]. " + f"Check: {domain}->{path}." + ) + else: + message += f"{humanize_error(config, ex)}." + is_friendly = True + else: + message += str(ex) or repr(ex) + + try: + domain_config = config.get(domain, config) + except AttributeError: + domain_config = config + + message += ( + f" (See {getattr(domain_config, '__config_file__', '?')}, " + f"line {getattr(domain_config, '__line__', '?')}). " + ) + + if domain != CONF_CORE and link: + message += f"Please check the docs at {link}" + + return message, is_friendly + + +async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> None: + """Process the [homeassistant] section from the configuration. + + This method is a coroutine. + """ + config = CORE_CONFIG_SCHEMA(config) + + # Only load auth during startup. + if not hasattr(hass, "auth"): + auth_conf = config.get(CONF_AUTH_PROVIDERS) + + if auth_conf is None: + auth_conf = [{"type": "homeassistant"}] + + mfa_conf = config.get( + CONF_AUTH_MFA_MODULES, + [{"type": "totp", "id": "totp", "name": "Authenticator app"}], + ) + + setattr( + hass, "auth", await auth.auth_manager_from_config(hass, auth_conf, mfa_conf) + ) + + await hass.config.async_load() + + hac = hass.config + + if any( + k in config + for k in [ + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + CONF_ELEVATION, + CONF_TIME_ZONE, + CONF_UNIT_SYSTEM, + CONF_EXTERNAL_URL, + CONF_INTERNAL_URL, + ] + ): + hac.config_source = SOURCE_YAML + + for key, attr in ( + (CONF_LATITUDE, "latitude"), + (CONF_LONGITUDE, "longitude"), + (CONF_NAME, "location_name"), + (CONF_ELEVATION, "elevation"), + (CONF_INTERNAL_URL, "internal_url"), + (CONF_EXTERNAL_URL, "external_url"), + (CONF_MEDIA_DIRS, "media_dirs"), + (CONF_LEGACY_TEMPLATES, "legacy_templates"), + ): + if key in config: + setattr(hac, attr, config[key]) + + if CONF_TIME_ZONE in config: + hac.set_time_zone(config[CONF_TIME_ZONE]) + + if CONF_MEDIA_DIRS not in config: + if is_docker_env(): + hac.media_dirs = {"local": "/media"} + else: + hac.media_dirs = {"local": hass.config.path("media")} + + # Init whitelist external dir + hac.allowlist_external_dirs = {hass.config.path("www"), *hac.media_dirs.values()} + if CONF_ALLOWLIST_EXTERNAL_DIRS in config: + hac.allowlist_external_dirs.update(set(config[CONF_ALLOWLIST_EXTERNAL_DIRS])) + + elif LEGACY_CONF_WHITELIST_EXTERNAL_DIRS in config: + _LOGGER.warning( + "Key %s has been replaced with %s. Please update your config", + LEGACY_CONF_WHITELIST_EXTERNAL_DIRS, + CONF_ALLOWLIST_EXTERNAL_DIRS, + ) + hac.allowlist_external_dirs.update( + set(config[LEGACY_CONF_WHITELIST_EXTERNAL_DIRS]) + ) + + # Init whitelist external URL list – make sure to add / to every URL that doesn't + # already have it so that we can properly test "path ownership" + if CONF_ALLOWLIST_EXTERNAL_URLS in config: + hac.allowlist_external_urls.update( + url if url.endswith("/") else f"{url}/" + for url in config[CONF_ALLOWLIST_EXTERNAL_URLS] + ) + + # Customize + cust_exact = dict(config[CONF_CUSTOMIZE]) + cust_domain = dict(config[CONF_CUSTOMIZE_DOMAIN]) + cust_glob = OrderedDict(config[CONF_CUSTOMIZE_GLOB]) + + for name, pkg in config[CONF_PACKAGES].items(): + pkg_cust = pkg.get(CONF_CORE) + + if pkg_cust is None: + continue + + try: + pkg_cust = CUSTOMIZE_CONFIG_SCHEMA(pkg_cust) + except vol.Invalid: + _LOGGER.warning("Package %s contains invalid customize", name) + continue + + cust_exact.update(pkg_cust[CONF_CUSTOMIZE]) + cust_domain.update(pkg_cust[CONF_CUSTOMIZE_DOMAIN]) + cust_glob.update(pkg_cust[CONF_CUSTOMIZE_GLOB]) + + hass.data[DATA_CUSTOMIZE] = EntityValues(cust_exact, cust_domain, cust_glob) + + if CONF_UNIT_SYSTEM in config: + if config[CONF_UNIT_SYSTEM] == CONF_UNIT_SYSTEM_IMPERIAL: + hac.units = IMPERIAL_SYSTEM + else: + hac.units = METRIC_SYSTEM + elif CONF_TEMPERATURE_UNIT in config: + unit = config[CONF_TEMPERATURE_UNIT] + hac.units = METRIC_SYSTEM if unit == TEMP_CELSIUS else IMPERIAL_SYSTEM + _LOGGER.warning( + "Found deprecated temperature unit in core " + "configuration expected unit system. Replace '%s: %s' " + "with '%s: %s'", + CONF_TEMPERATURE_UNIT, + unit, + CONF_UNIT_SYSTEM, + hac.units.name, + ) + + +def _log_pkg_error(package: str, component: str, config: dict, message: str) -> None: + """Log an error while merging packages.""" + message = f"Package {package} setup failed. Integration {component} {message}" + + pack_config = config[CONF_CORE][CONF_PACKAGES].get(package, config) + message += ( + f" (See {getattr(pack_config, '__config_file__', '?')}:" + f"{getattr(pack_config, '__line__', '?')}). " + ) + + _LOGGER.error(message) + + +def _identify_config_schema(module: ModuleType) -> str | None: + """Extract the schema and identify list or dict based.""" + if not isinstance(module.CONFIG_SCHEMA, vol.Schema): # type: ignore + return None + + schema = module.CONFIG_SCHEMA.schema # type: ignore + + if isinstance(schema, vol.All): + for subschema in schema.validators: + if isinstance(subschema, dict): + schema = subschema + break + else: + return None + + try: + key = next(k for k in schema if k == module.DOMAIN) # type: ignore + except (TypeError, AttributeError, StopIteration): + return None + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected error identifying config schema") + return None + + if hasattr(key, "default") and not isinstance( + key.default, vol.schema_builder.Undefined + ): + default_value = module.CONFIG_SCHEMA({module.DOMAIN: key.default()})[ # type: ignore + module.DOMAIN # type: ignore + ] + + if isinstance(default_value, dict): + return "dict" + + if isinstance(default_value, list): + return "list" + + return None + + domain_schema = schema[key] + + t_schema = str(domain_schema) + if t_schema.startswith("{") or "schema_with_slug_keys" in t_schema: + return "dict" + if t_schema.startswith(("[", "All( bool | str: + """Merge package into conf, recursively.""" + error: bool | str = False + for key, pack_conf in package.items(): + if isinstance(pack_conf, dict): + if not pack_conf: + continue + conf[key] = conf.get(key, OrderedDict()) + error = _recursive_merge(conf=conf[key], package=pack_conf) + + elif isinstance(pack_conf, list): + conf[key] = cv.remove_falsy( + cv.ensure_list(conf.get(key)) + cv.ensure_list(pack_conf) + ) + + else: + if conf.get(key) is not None: + return key + conf[key] = pack_conf + return error + + +async def merge_packages_config( + hass: HomeAssistant, + config: dict, + packages: dict[str, Any], + _log_pkg_error: Callable = _log_pkg_error, +) -> dict: + """Merge packages into the top-level configuration. Mutate config.""" + PACKAGES_CONFIG_SCHEMA(packages) + for pack_name, pack_conf in packages.items(): + for comp_name, comp_conf in pack_conf.items(): + if comp_name == CONF_CORE: + continue + # If component name is given with a trailing description, remove it + # when looking for component + domain = comp_name.split(" ")[0] + + try: + integration = await async_get_integration_with_requirements( + hass, domain + ) + component = integration.get_component() + except INTEGRATION_LOAD_EXCEPTIONS as ex: + _log_pkg_error(pack_name, comp_name, config, str(ex)) + continue + + merge_list = hasattr(component, "PLATFORM_SCHEMA") + + if not merge_list and hasattr(component, "CONFIG_SCHEMA"): + merge_list = _identify_config_schema(component) == "list" + + if merge_list: + config[comp_name] = cv.remove_falsy( + cv.ensure_list(config.get(comp_name)) + cv.ensure_list(comp_conf) + ) + continue + + if comp_conf is None: + comp_conf = OrderedDict() + + if not isinstance(comp_conf, dict): + _log_pkg_error( + pack_name, comp_name, config, "cannot be merged. Expected a dict." + ) + continue + + if comp_name not in config or config[comp_name] is None: + config[comp_name] = OrderedDict() + + if not isinstance(config[comp_name], dict): + _log_pkg_error( + pack_name, + comp_name, + config, + "cannot be merged. Dict expected in main config.", + ) + continue + + error = _recursive_merge(conf=config[comp_name], package=comp_conf) + if error: + _log_pkg_error( + pack_name, comp_name, config, f"has duplicate key '{error}'" + ) + + return config + + +async def async_process_component_config( # noqa: C901 + hass: HomeAssistant, config: ConfigType, integration: Integration +) -> ConfigType | None: + """Check component configuration and return processed configuration. + + Returns None on error. + + This method must be run in the event loop. + """ + domain = integration.domain + try: + component = integration.get_component() + except LOAD_EXCEPTIONS as ex: + _LOGGER.error("Unable to import %s: %s", domain, ex) + return None + + # Check if the integration has a custom config validator + config_validator = None + try: + config_validator = integration.get_platform("config") + except ImportError as err: + # Filter out import error of the config platform. + # If the config platform contains bad imports, make sure + # that still fails. + if err.name != f"{integration.pkg_path}.config": + _LOGGER.error("Error importing config platform %s: %s", domain, err) + return None + + if config_validator is not None and hasattr( + config_validator, "async_validate_config" + ): + try: + return await config_validator.async_validate_config( # type: ignore + hass, config + ) + except (vol.Invalid, HomeAssistantError) as ex: + async_log_exception(ex, domain, config, hass, integration.documentation) + return None + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error calling %s config validator", domain) + return None + + # No custom config validator, proceed with schema validation + if hasattr(component, "CONFIG_SCHEMA"): + try: + return component.CONFIG_SCHEMA(config) # type: ignore + except vol.Invalid as ex: + async_log_exception(ex, domain, config, hass, integration.documentation) + return None + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error calling %s CONFIG_SCHEMA", domain) + return None + + component_platform_schema = getattr( + component, "PLATFORM_SCHEMA_BASE", getattr(component, "PLATFORM_SCHEMA", None) + ) + + if component_platform_schema is None: + return config + + platforms = [] + for p_name, p_config in config_per_platform(config, domain): + # Validate component specific platform schema + try: + p_validated = component_platform_schema(p_config) + except vol.Invalid as ex: + async_log_exception(ex, domain, p_config, hass, integration.documentation) + continue + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Unknown error validating %s platform config with %s component platform schema", + p_name, + domain, + ) + continue + + # Not all platform components follow same pattern for platforms + # So if p_name is None we are not going to validate platform + # (the automation component is one of them) + if p_name is None: + platforms.append(p_validated) + continue + + try: + p_integration = await async_get_integration_with_requirements(hass, p_name) + except (RequirementsNotFound, IntegrationNotFound) as ex: + _LOGGER.error("Platform error: %s - %s", domain, ex) + continue + + try: + platform = p_integration.get_platform(domain) + except LOAD_EXCEPTIONS: + _LOGGER.exception("Platform error: %s", domain) + continue + + # Validate platform specific schema + if hasattr(platform, "PLATFORM_SCHEMA"): + try: + p_validated = platform.PLATFORM_SCHEMA(p_config) # type: ignore + except vol.Invalid as ex: + async_log_exception( + ex, + f"{domain}.{p_name}", + p_config, + hass, + p_integration.documentation, + ) + continue + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Unknown error validating config for %s platform for %s component with PLATFORM_SCHEMA", + p_name, + domain, + ) + continue + + platforms.append(p_validated) + + # Create a copy of the configuration with all config for current + # component removed and add validated config back in. + config = config_without_domain(config, domain) + config[domain] = platforms + + return config + + +@callback +def config_without_domain(config: dict, domain: str) -> dict: + """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} + + +async def async_check_ha_config_file(hass: HomeAssistant) -> str | None: + """Check if Home Assistant configuration file is valid. + + This method is a coroutine. + """ + # pylint: disable=import-outside-toplevel + import homeassistant.helpers.check_config as check_config + + res = await check_config.async_check_ha_config_file(hass) + + if not res.errors: + return None + return res.error_str + + +@callback +def async_notify_setup_error( + hass: HomeAssistant, component: str, display_link: str | None = None +) -> None: + """Print a persistent notification. + + This method must be run in the event loop. + """ + # pylint: disable=import-outside-toplevel + from homeassistant.components import persistent_notification + + errors = hass.data.get(DATA_PERSISTENT_ERRORS) + + if errors is None: + errors = hass.data[DATA_PERSISTENT_ERRORS] = {} + + errors[component] = errors.get(component) or display_link + + message = "The following integrations and platforms could not be set up:\n\n" + + for name, link in errors.items(): + part = f"[{name}]({link})" if link else name + message += f" - {part}\n" + + message += "\nPlease check your config and [logs](/config/logs)." + + persistent_notification.async_create( + hass, message, "Invalid config", "invalid_config" + ) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/config_entries.py b/homeassistant-2021.6.0.dev0/homeassistant/config_entries.py new file mode 100644 index 00000000000..c586fcad79f --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/config_entries.py @@ -0,0 +1,1494 @@ +"""Manage config entries in Home Assistant.""" +from __future__ import annotations + +import asyncio +from collections.abc import Iterable, Mapping +from contextvars import ContextVar +import functools +import logging +from types import MappingProxyType, MethodType +from typing import Any, Callable, Optional, cast +import weakref + +import attr + +from homeassistant import data_entry_flow, loader +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) +from homeassistant.helpers import device_registry, entity_registry +from homeassistant.helpers.event import Event +from homeassistant.helpers.typing import UNDEFINED, 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 + +_LOGGER = logging.getLogger(__name__) + +SOURCE_DISCOVERY = "discovery" +SOURCE_HASSIO = "hassio" +SOURCE_HOMEKIT = "homekit" +SOURCE_IMPORT = "import" +SOURCE_INTEGRATION_DISCOVERY = "integration_discovery" +SOURCE_MQTT = "mqtt" +SOURCE_SSDP = "ssdp" +SOURCE_USER = "user" +SOURCE_ZEROCONF = "zeroconf" +SOURCE_DHCP = "dhcp" + +# If a user wants to hide a discovery from the UI they can "Ignore" it. The config_entries/ignore_flow +# websocket command creates a config entry with this source and while it exists normal discoveries +# with the same unique id are ignored. +SOURCE_IGNORE = "ignore" + +# This is used when a user uses the "Stop Ignoring" button in the UI (the +# config_entries/ignore_flow websocket command). It's triggered after the "ignore" config entry has +# been removed and unloaded. +SOURCE_UNIGNORE = "unignore" + +# This is used to signal that re-authentication is required by the user. +SOURCE_REAUTH = "reauth" + +HANDLERS = Registry() + +STORAGE_KEY = "core.config_entries" +STORAGE_VERSION = 1 + +# Deprecated since 0.73 +PATH_CONFIG = ".config_entries.json" + +SAVE_DELAY = 1 + +# The config entry has been set up successfully +ENTRY_STATE_LOADED = "loaded" +# There was an error while trying to set up this config entry +ENTRY_STATE_SETUP_ERROR = "setup_error" +# There was an error while trying to migrate the config entry to a new version +ENTRY_STATE_MIGRATION_ERROR = "migration_error" +# The config entry was not ready to be set up yet, but might be later +ENTRY_STATE_SETUP_RETRY = "setup_retry" +# The config entry has not been loaded +ENTRY_STATE_NOT_LOADED = "not_loaded" +# An error occurred when trying to unload the entry +ENTRY_STATE_FAILED_UNLOAD = "failed_unload" + +UNRECOVERABLE_STATES = (ENTRY_STATE_MIGRATION_ERROR, ENTRY_STATE_FAILED_UNLOAD) + +DEFAULT_DISCOVERY_UNIQUE_ID = "default_discovery_unique_id" +DISCOVERY_NOTIFICATION_ID = "config_entry_discovery" +DISCOVERY_SOURCES = ( + SOURCE_SSDP, + SOURCE_ZEROCONF, + SOURCE_DISCOVERY, + SOURCE_IMPORT, + SOURCE_UNIGNORE, +) + +RECONFIGURE_NOTIFICATION_ID = "config_entry_reconfigure" + +EVENT_FLOW_DISCOVERED = "config_entry_discovered" + +DISABLED_USER = "user" + +RELOAD_AFTER_UPDATE_DELAY = 30 + +# Deprecated: Connection classes +# These aren't used anymore since 2021.6.0 +# Mainly here not to break custom integrations. +CONN_CLASS_CLOUD_PUSH = "cloud_push" +CONN_CLASS_CLOUD_POLL = "cloud_poll" +CONN_CLASS_LOCAL_PUSH = "local_push" +CONN_CLASS_LOCAL_POLL = "local_poll" +CONN_CLASS_ASSUMED = "assumed" +CONN_CLASS_UNKNOWN = "unknown" + + +class ConfigError(HomeAssistantError): + """Error while configuring an account.""" + + +class UnknownEntry(ConfigError): + """Unknown entry specified.""" + + +class OperationNotAllowed(ConfigError): + """Raised when a config entry operation is not allowed.""" + + +UpdateListenerType = Callable[[HomeAssistant, "ConfigEntry"], Any] + + +class ConfigEntry: + """Hold a configuration entry.""" + + __slots__ = ( + "entry_id", + "version", + "domain", + "title", + "data", + "options", + "unique_id", + "supports_unload", + "system_options", + "source", + "state", + "disabled_by", + "_setup_lock", + "update_listeners", + "reason", + "_async_cancel_retry_setup", + "_on_unload", + ) + + def __init__( + self, + version: int, + domain: str, + title: str, + data: Mapping[str, Any], + source: str, + system_options: dict, + options: Mapping[str, Any] | None = None, + unique_id: str | None = None, + entry_id: str | None = None, + state: str = ENTRY_STATE_NOT_LOADED, + disabled_by: str | None = None, + ) -> None: + """Initialize a config entry.""" + # Unique id of the config entry + self.entry_id = entry_id or uuid_util.random_uuid_hex() + + # Version of the configuration. + self.version = version + + # Domain the configuration belongs to + self.domain = domain + + # Title of the configuration + self.title = title + + # Config data + self.data = MappingProxyType(data) + + # Entry options + self.options = MappingProxyType(options or {}) + + # Entry system options + self.system_options = SystemOptions(**system_options) + + # Source of the configuration (user, discovery, cloud) + self.source = source + + # State of the entry (LOADED, NOT_LOADED) + self.state = state + + # Unique ID of this entry. + self.unique_id = unique_id + + # Config entry is disabled + self.disabled_by = disabled_by + + # Supports unload + self.supports_unload = False + + # Listeners to call on update + self.update_listeners: list[ + weakref.ReferenceType[UpdateListenerType] | weakref.WeakMethod + ] = [] + + # Reason why config entry is in a failed state + self.reason: str | None = None + + # Function to cancel a scheduled retry + self._async_cancel_retry_setup: Callable[[], Any] | None = None + + # Hold list for functions to call on unload. + self._on_unload: list[CALLBACK_TYPE] | None = None + + async def async_setup( + self, + hass: HomeAssistant, + *, + integration: loader.Integration | None = None, + tries: int = 0, + ) -> None: + """Set up an entry.""" + current_entry.set(self) + if self.source == SOURCE_IGNORE or self.disabled_by: + return + + if integration is None: + integration = await loader.async_get_integration(hass, self.domain) + + self.supports_unload = await support_entry_unload(hass, self.domain) + + try: + component = integration.get_component() + except ImportError as err: + _LOGGER.error( + "Error importing integration %s to set up %s configuration entry: %s", + integration.domain, + self.domain, + err, + ) + if self.domain == integration.domain: + self.state = ENTRY_STATE_SETUP_ERROR + self.reason = "Import error" + return + + if self.domain == integration.domain: + try: + integration.get_platform("config_flow") + except ImportError as err: + _LOGGER.error( + "Error importing platform config_flow from integration %s to set up %s configuration entry: %s", + integration.domain, + self.domain, + err, + ) + self.state = ENTRY_STATE_SETUP_ERROR + self.reason = "Import error" + return + + # Perform migration + if not await self.async_migrate(hass): + self.state = ENTRY_STATE_MIGRATION_ERROR + self.reason = None + return + + error_reason = None + + try: + result = await component.async_setup_entry(hass, self) # type: ignore + + if not isinstance(result, bool): + _LOGGER.error( + "%s.async_setup_entry did not return boolean", integration.domain + ) + result = False + except ConfigEntryAuthFailed as ex: + message = str(ex) + auth_base_message = "could not authenticate" + error_reason = message or auth_base_message + auth_message = ( + f"{auth_base_message}: {message}" if message else auth_base_message + ) + _LOGGER.warning( + "Config entry '%s' for %s integration %s", + self.title, + self.domain, + auth_message, + ) + self._async_process_on_unload() + self.async_start_reauth(hass) + result = False + except ConfigEntryNotReady as ex: + self.state = ENTRY_STATE_SETUP_RETRY + self.reason = str(ex) or None + wait_time = 2 ** min(tries, 4) * 5 + tries += 1 + message = str(ex) + ready_message = f"ready yet: {message}" if message else "ready yet" + if tries == 1: + _LOGGER.warning( + "Config entry '%s' for %s integration not %s; Retrying in background", + self.title, + self.domain, + ready_message, + ) + else: + _LOGGER.debug( + "Config entry '%s' for %s integration not %s; Retrying in %d seconds", + self.title, + self.domain, + ready_message, + wait_time, + ) + + async def setup_again(*_: Any) -> None: + """Run setup again.""" + self._async_cancel_retry_setup = None + await self.async_setup(hass, integration=integration, tries=tries) + + if hass.state == CoreState.running: + self._async_cancel_retry_setup = hass.helpers.event.async_call_later( + wait_time, setup_again + ) + else: + self._async_cancel_retry_setup = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, setup_again + ) + + self._async_process_on_unload() + return + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Error setting up entry %s for %s", self.title, integration.domain + ) + result = False + + # Only store setup result as state if it was not forwarded. + if self.domain != integration.domain: + return + + if result: + self.state = ENTRY_STATE_LOADED + self.reason = None + else: + self.state = ENTRY_STATE_SETUP_ERROR + self.reason = error_reason + + async def async_shutdown(self) -> None: + """Call when Home Assistant is stopping.""" + self.async_cancel_retry_setup() + + @callback + def async_cancel_retry_setup(self) -> None: + """Cancel retry setup.""" + if self._async_cancel_retry_setup is not None: + self._async_cancel_retry_setup() + self._async_cancel_retry_setup = None + + async def async_unload( + self, hass: HomeAssistant, *, integration: loader.Integration | None = None + ) -> bool: + """Unload an entry. + + Returns if unload is possible and was successful. + """ + if self.source == SOURCE_IGNORE: + self.state = ENTRY_STATE_NOT_LOADED + self.reason = None + return True + + if integration is None: + try: + integration = await loader.async_get_integration(hass, self.domain) + except loader.IntegrationNotFound: + # The integration was likely a custom_component + # that was uninstalled, or an integration + # that has been renamed without removing the config + # entry. + self.state = ENTRY_STATE_NOT_LOADED + self.reason = None + return True + + component = integration.get_component() + + if integration.domain == self.domain: + if self.state in UNRECOVERABLE_STATES: + return False + + if self.state != ENTRY_STATE_LOADED: + self.async_cancel_retry_setup() + + self.state = ENTRY_STATE_NOT_LOADED + self.reason = None + return True + + supports_unload = hasattr(component, "async_unload_entry") + + if not supports_unload: + if integration.domain == self.domain: + self.state = ENTRY_STATE_FAILED_UNLOAD + self.reason = "Unload not supported" + return False + + try: + result = await component.async_unload_entry(hass, self) # type: ignore + + assert isinstance(result, bool) + + # Only adjust state if we unloaded the component + if result and integration.domain == self.domain: + self.state = ENTRY_STATE_NOT_LOADED + self.reason = None + + self._async_process_on_unload() + + return result + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Error unloading entry %s for %s", self.title, integration.domain + ) + if integration.domain == self.domain: + self.state = ENTRY_STATE_FAILED_UNLOAD + self.reason = "Unknown error" + return False + + async def async_remove(self, hass: HomeAssistant) -> None: + """Invoke remove callback on component.""" + if self.source == SOURCE_IGNORE: + return + + try: + integration = await loader.async_get_integration(hass, self.domain) + except loader.IntegrationNotFound: + # The integration was likely a custom_component + # that was uninstalled, or an integration + # that has been renamed without removing the config + # entry. + return + + component = integration.get_component() + if not hasattr(component, "async_remove_entry"): + return + try: + await component.async_remove_entry(hass, self) # type: ignore + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Error calling entry remove callback %s for %s", + self.title, + integration.domain, + ) + + async def async_migrate(self, hass: HomeAssistant) -> bool: + """Migrate an entry. + + Returns True if config entry is up-to-date or has been migrated. + """ + handler = HANDLERS.get(self.domain) + if handler is None: + _LOGGER.error( + "Flow handler not found for entry %s for %s", self.title, self.domain + ) + return False + # Handler may be a partial + while isinstance(handler, functools.partial): + handler = handler.func + + if self.version == handler.VERSION: + return True + + integration = await loader.async_get_integration(hass, self.domain) + component = integration.get_component() + supports_migrate = hasattr(component, "async_migrate_entry") + if not supports_migrate: + _LOGGER.error( + "Migration handler not found for entry %s for %s", + self.title, + self.domain, + ) + return False + + try: + result = await component.async_migrate_entry(hass, self) # type: ignore + if not isinstance(result, bool): + _LOGGER.error( + "%s.async_migrate_entry did not return boolean", self.domain + ) + return False + if result: + # pylint: disable=protected-access + hass.config_entries._async_schedule_save() + return result + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Error migrating entry %s for %s", self.title, self.domain + ) + return False + + def add_update_listener(self, listener: UpdateListenerType) -> CALLBACK_TYPE: + """Listen for when entry is updated. + + Returns function to unlisten. + """ + weak_listener: Any + # weakref.ref is not applicable to a bound method, e.g. method of a class instance, as reference will die immediately + if hasattr(listener, "__self__"): + weak_listener = weakref.WeakMethod(cast(MethodType, listener)) + else: + weak_listener = weakref.ref(listener) + self.update_listeners.append(weak_listener) + + return lambda: self.update_listeners.remove(weak_listener) + + def as_dict(self) -> dict[str, Any]: + """Return dictionary version of this entry.""" + return { + "entry_id": self.entry_id, + "version": self.version, + "domain": self.domain, + "title": self.title, + "data": dict(self.data), + "options": dict(self.options), + "system_options": self.system_options.as_dict(), + "source": self.source, + "unique_id": self.unique_id, + "disabled_by": self.disabled_by, + } + + @callback + def async_on_unload(self, func: CALLBACK_TYPE) -> None: + """Add a function to call when config entry is unloaded.""" + if self._on_unload is None: + self._on_unload = [] + self._on_unload.append(func) + + @callback + def _async_process_on_unload(self) -> None: + """Process the on_unload callbacks.""" + if self._on_unload is not None: + while self._on_unload: + self._on_unload.pop()() + + @callback + def async_start_reauth(self, hass: HomeAssistant) -> None: + """Start a reauth flow.""" + flow_context = { + "source": SOURCE_REAUTH, + "entry_id": self.entry_id, + "unique_id": self.unique_id, + } + + for flow in hass.config_entries.flow.async_progress(): + if flow["context"] == flow_context: + return + + hass.async_create_task( + hass.config_entries.flow.async_init( + self.domain, + context=flow_context, + data=self.data, + ) + ) + + +current_entry: ContextVar[ConfigEntry | None] = ContextVar( + "current_entry", default=None +) + + +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 + ): + """Initialize the config entry flow manager.""" + super().__init__(hass) + self.config_entries = config_entries + self._hass_config = hass_config + + async def async_finish_flow( + self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult + ) -> data_entry_flow.FlowResult: + """Finish a config flow and add an entry.""" + flow = cast(ConfigFlow, flow) + + # Remove notification if no other discovery config entries in progress + if not any( + ent["context"]["source"] in DISCOVERY_SOURCES + for ent in self.hass.config_entries.flow.async_progress() + if ent["flow_id"] != flow.flow_id + ): + self.hass.components.persistent_notification.async_dismiss( + DISCOVERY_NOTIFICATION_ID + ) + + if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return result + + # Check if config entry exists with unique ID. Unload it. + existing_entry = None + + if flow.unique_id is not None: + # Abort all flows in progress with same unique ID. + for progress_flow in self.async_progress(): + if ( + progress_flow["handler"] == flow.handler + and progress_flow["flow_id"] != flow.flow_id + and progress_flow["context"].get("unique_id") == flow.unique_id + ): + self.async_abort(progress_flow["flow_id"]) + + # Reset unique ID when the default discovery ID has been used + if flow.unique_id == DEFAULT_DISCOVERY_UNIQUE_ID: + await flow.async_set_unique_id(None) + + # Find existing entry. + for check_entry in self.config_entries.async_entries(result["handler"]): + if check_entry.unique_id == flow.unique_id: + existing_entry = check_entry + break + + # Unload the entry before setting up the new one. + # We will remove it only after the other one is set up, + # so that device customizations are not getting lost. + if ( + existing_entry is not None + and existing_entry.state not in UNRECOVERABLE_STATES + ): + await self.config_entries.async_unload(existing_entry.entry_id) + + entry = ConfigEntry( + version=result["version"], + domain=result["handler"], + title=result["title"], + data=result["data"], + options=result["options"], + system_options={}, + source=flow.context["source"], + unique_id=flow.unique_id, + ) + + await self.config_entries.async_add(entry) + + if existing_entry is not None: + await self.config_entries.async_remove(existing_entry.entry_id) + + result["result"] = entry + return result + + async def async_create_flow( + self, handler_key: Any, *, context: dict | None = None, data: Any = None + ) -> ConfigFlow: + """Create a flow for specified handler. + + Handler key is the domain of the component that we want to set up. + """ + try: + integration = await loader.async_get_integration(self.hass, handler_key) + except loader.IntegrationNotFound as err: + _LOGGER.error("Cannot find integration %s", handler_key) + raise data_entry_flow.UnknownHandler from err + + # Make sure requirements and dependencies of component are resolved + await async_process_deps_reqs(self.hass, self._hass_config, integration) + + try: + integration.get_platform("config_flow") + except ImportError as err: + _LOGGER.error( + "Error occurred loading configuration flow for integration %s: %s", + handler_key, + err, + ) + raise data_entry_flow.UnknownHandler + + handler = HANDLERS.get(handler_key) + + if handler is None: + raise data_entry_flow.UnknownHandler + + if not context or "source" not in context: + raise KeyError("Context not set or doesn't have a source set") + + flow = cast(ConfigFlow, handler()) + flow.init_step = context["source"] + return flow + + async def async_post_init( + self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult + ) -> None: + """After a flow is initialised trigger new flow notifications.""" + source = flow.context["source"] + + # Create notification. + if source in DISCOVERY_SOURCES: + self.hass.bus.async_fire(EVENT_FLOW_DISCOVERED) + self.hass.components.persistent_notification.async_create( + title="New devices discovered", + message=( + "We have discovered new devices on your network. " + "[Check it out](/config/integrations)." + ), + notification_id=DISCOVERY_NOTIFICATION_ID, + ) + elif source == SOURCE_REAUTH: + self.hass.components.persistent_notification.async_create( + title="Integration requires reconfiguration", + message=( + "At least one of your integrations requires reconfiguration to " + "continue functioning. [Check it out](/config/integrations)." + ), + notification_id=RECONFIGURE_NOTIFICATION_ID, + ) + + +class ConfigEntries: + """Manage the configuration entries. + + An instance of this object is available via `hass.config_entries`. + """ + + def __init__(self, hass: HomeAssistant, hass_config: dict) -> None: + """Initialize the entry manager.""" + self.hass = hass + self.flow = ConfigEntriesFlowManager(hass, self, hass_config) + self.options = OptionsFlowManager(hass) + self._hass_config = hass_config + self._entries: dict[str, ConfigEntry] = {} + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + EntityRegistryDisabledHandler(hass).async_setup() + + @callback + def async_domains( + self, include_ignore: bool = False, include_disabled: bool = False + ) -> list[str]: + """Return domains for which we have entries.""" + return list( + { + entry.domain: None + for entry in self._entries.values() + if (include_ignore or entry.source != SOURCE_IGNORE) + and (include_disabled or not entry.disabled_by) + } + ) + + @callback + def async_get_entry(self, entry_id: str) -> ConfigEntry | None: + """Return entry with matching entry_id.""" + return self._entries.get(entry_id) + + @callback + def async_entries(self, domain: str | None = None) -> list[ConfigEntry]: + """Return all entries or entries for a specific domain.""" + if domain is None: + return list(self._entries.values()) + return [entry for entry in self._entries.values() if entry.domain == domain] + + async def async_add(self, entry: ConfigEntry) -> None: + """Add and setup an entry.""" + if entry.entry_id in self._entries: + raise HomeAssistantError( + f"An entry with the id {entry.entry_id} already exists." + ) + self._entries[entry.entry_id] = entry + await self.async_setup(entry.entry_id) + self._async_schedule_save() + + async def async_remove(self, entry_id: str) -> dict[str, Any]: + """Remove an entry.""" + entry = self.async_get_entry(entry_id) + + if entry is None: + raise UnknownEntry + + if entry.state in UNRECOVERABLE_STATES: + unload_success = entry.state != ENTRY_STATE_FAILED_UNLOAD + else: + unload_success = await self.async_unload(entry_id) + + await entry.async_remove(self.hass) + + del self._entries[entry.entry_id] + self._async_schedule_save() + + dev_reg, ent_reg = await asyncio.gather( + self.hass.helpers.device_registry.async_get_registry(), + self.hass.helpers.entity_registry.async_get_registry(), + ) + + dev_reg.async_clear_config_entry(entry_id) + ent_reg.async_clear_config_entry(entry_id) + + # After we have fully removed an "ignore" config entry we can try and rediscover it so that a + # user is able to immediately start configuring it. We do this by starting a new flow with + # the 'unignore' step. If the integration doesn't implement async_step_unignore then + # this will be a no-op. + if entry.source == SOURCE_IGNORE: + self.hass.async_create_task( + self.hass.config_entries.flow.async_init( + entry.domain, + context={"source": SOURCE_UNIGNORE}, + data={"unique_id": entry.unique_id}, + ) + ) + + return {"require_restart": not unload_success} + + async def _async_shutdown(self, event: Event) -> None: + """Call when Home Assistant is stopping.""" + await asyncio.gather( + *[entry.async_shutdown() for entry in self._entries.values()] + ) + await self.flow.async_shutdown() + + async def async_initialize(self) -> None: + """Initialize config entry config.""" + # Migrating for config entries stored before 0.73 + config = await self.hass.helpers.storage.async_migrator( + self.hass.config.path(PATH_CONFIG), + self._store, + old_conf_migrate_func=_old_conf_migrator, + ) + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_shutdown) + + if config is None: + self._entries = {} + return + + self._entries = { + entry["entry_id"]: ConfigEntry( + version=entry["version"], + domain=entry["domain"], + entry_id=entry["entry_id"], + data=entry["data"], + source=entry["source"], + title=entry["title"], + # New in 0.89 + options=entry.get("options"), + # New in 0.98 + system_options=entry.get("system_options", {}), + # New in 0.104 + unique_id=entry.get("unique_id"), + # New in 2021.3 + disabled_by=entry.get("disabled_by"), + ) + for entry in config["entries"] + } + + async def async_setup(self, entry_id: str) -> bool: + """Set up a config entry. + + Return True if entry has been successfully loaded. + """ + entry = self.async_get_entry(entry_id) + + if entry is None: + raise UnknownEntry + + if entry.state != ENTRY_STATE_NOT_LOADED: + raise OperationNotAllowed + + # Setup Component if not set up yet + if entry.domain in self.hass.config.components: + await entry.async_setup(self.hass) + else: + # Setting up the component will set up all its config entries + result = await async_setup_component( + self.hass, entry.domain, self._hass_config + ) + + if not result: + return result + + return entry.state == ENTRY_STATE_LOADED + + async def async_unload(self, entry_id: str) -> bool: + """Unload a config entry.""" + entry = self.async_get_entry(entry_id) + + if entry is None: + raise UnknownEntry + + if entry.state in UNRECOVERABLE_STATES: + raise OperationNotAllowed + + return await entry.async_unload(self.hass) + + async def async_reload(self, entry_id: str) -> bool: + """Reload an entry. + + If an entry was not loaded, will just load. + """ + entry = self.async_get_entry(entry_id) + + if entry is None: + raise UnknownEntry + + unload_result = await self.async_unload(entry_id) + + if not unload_result or entry.disabled_by: + return unload_result + + return await self.async_setup(entry_id) + + async def async_set_disabled_by( + self, entry_id: str, disabled_by: str | None + ) -> bool: + """Disable an entry. + + If disabled_by is changed, the config entry will be reloaded. + """ + entry = self.async_get_entry(entry_id) + + if entry is None: + raise UnknownEntry + + if entry.disabled_by == disabled_by: + return True + + entry.disabled_by = disabled_by + self._async_schedule_save() + + dev_reg = device_registry.async_get(self.hass) + ent_reg = entity_registry.async_get(self.hass) + + if not entry.disabled_by: + # The config entry will no longer be disabled, enable devices and entities + device_registry.async_config_entry_disabled_by_changed(dev_reg, entry) + entity_registry.async_config_entry_disabled_by_changed(ent_reg, entry) + + # Load or unload the config entry + reload_result = await self.async_reload(entry_id) + + if entry.disabled_by: + # The config entry has been disabled, disable devices and entities + device_registry.async_config_entry_disabled_by_changed(dev_reg, entry) + entity_registry.async_config_entry_disabled_by_changed(ent_reg, entry) + + return reload_result + + @callback + def async_update_entry( + self, + entry: ConfigEntry, + *, + unique_id: str | dict | None | UndefinedType = UNDEFINED, + title: str | dict | UndefinedType = UNDEFINED, + data: dict | UndefinedType = UNDEFINED, + options: Mapping[str, Any] | UndefinedType = UNDEFINED, + system_options: dict | UndefinedType = UNDEFINED, + ) -> bool: + """Update a config entry. + + If the entry was changed, the update_listeners are + fired and this function returns True + + If the entry was not changed, the update_listeners are + not fired and this function returns False + """ + changed = False + + if unique_id is not UNDEFINED and entry.unique_id != unique_id: + changed = True + entry.unique_id = cast(Optional[str], unique_id) + + if title is not UNDEFINED and entry.title != title: + changed = True + entry.title = cast(str, title) + + if data is not UNDEFINED and entry.data != data: # type: ignore + changed = True + entry.data = MappingProxyType(data) + + if options is not UNDEFINED and entry.options != options: + changed = True + entry.options = MappingProxyType(options) + + if ( + system_options is not UNDEFINED + and entry.system_options.as_dict() != system_options + ): + changed = True + entry.system_options.update(**system_options) + + if not changed: + return False + + for listener_ref in entry.update_listeners: + listener = listener_ref() + if listener is not None: + self.hass.async_create_task(listener(self.hass, entry)) + + self._async_schedule_save() + + return True + + @callback + def async_setup_platforms( + self, entry: ConfigEntry, platforms: Iterable[str] + ) -> None: + """Forward the setup of an entry to platforms.""" + for platform in platforms: + self.hass.async_create_task(self.async_forward_entry_setup(entry, platform)) + + async def async_forward_entry_setup(self, entry: ConfigEntry, domain: str) -> bool: + """Forward the setup of an entry to a different component. + + By default an entry is setup with the component it belongs to. If that + component also has related platforms, the component will have to + forward the entry to be setup by that component. + + You don't want to await this coroutine if it is called as part of the + setup of a component, because it can cause a deadlock. + """ + # Setup Component if not set up yet + if domain not in self.hass.config.components: + result = await async_setup_component(self.hass, domain, self._hass_config) + + if not result: + return False + + integration = await loader.async_get_integration(self.hass, domain) + + await entry.async_setup(self.hass, integration=integration) + return True + + async def async_unload_platforms( + self, entry: ConfigEntry, platforms: Iterable[str] + ) -> bool: + """Forward the unloading of an entry to platforms.""" + return all( + await asyncio.gather( + *[ + self.async_forward_entry_unload(entry, platform) + for platform in platforms + ] + ) + ) + + async def async_forward_entry_unload(self, entry: ConfigEntry, domain: str) -> bool: + """Forward the unloading of an entry to a different component.""" + # It was never loaded. + if domain not in self.hass.config.components: + return True + + integration = await loader.async_get_integration(self.hass, domain) + + return await entry.async_unload(self.hass, integration=integration) + + @callback + def _async_schedule_save(self) -> None: + """Save the entity registry to a file.""" + self._store.async_delay_save(self._data_to_save, SAVE_DELAY) + + @callback + def _data_to_save(self) -> dict[str, list[dict[str, Any]]]: + """Return data to save.""" + return {"entries": [entry.as_dict() for entry in self._entries.values()]} + + +async def _old_conf_migrator(old_config: dict[str, Any]) -> dict[str, Any]: + """Migrate the pre-0.73 config format to the latest version.""" + return {"entries": old_config} + + +class ConfigFlow(data_entry_flow.FlowHandler): + """Base class for config flows with some helpers.""" + + def __init_subclass__(cls, domain: str | None = None, **kwargs: Any) -> None: + """Initialize a subclass, register if possible.""" + super().__init_subclass__(**kwargs) # type: ignore + if domain is not None: + HANDLERS.register(domain)(cls) + + @property + def unique_id(self) -> str | None: + """Return unique ID if available.""" + if not self.context: + return None + + return cast(Optional[str], self.context.get("unique_id")) + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: + """Get the options flow for this handler.""" + raise data_entry_flow.UnknownHandler + + @callback + def _async_abort_entries_match( + self, match_dict: dict[str, Any] | None = None + ) -> None: + """Abort if current entries match all data.""" + if match_dict is None: + match_dict = {} # Match any entry + for entry in self._async_current_entries(include_ignore=False): + if all(item in entry.data.items() for item in match_dict.items()): + raise data_entry_flow.AbortFlow("already_configured") + + @callback + def _abort_if_unique_id_configured( + self, + updates: dict[Any, Any] | None = None, + reload_on_update: bool = True, + ) -> None: + """Abort if the unique ID is already configured.""" + if self.unique_id is None: + return + + for entry in self._async_current_entries(include_ignore=True): + if entry.unique_id == self.unique_id: + if updates is not None: + changed = self.hass.config_entries.async_update_entry( + entry, data={**entry.data, **updates} + ) + if ( + changed + and reload_on_update + and entry.state in (ENTRY_STATE_LOADED, ENTRY_STATE_SETUP_RETRY) + ): + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + # Allow ignored entries to be configured on manual user step + if entry.source == SOURCE_IGNORE and self.source == SOURCE_USER: + continue + raise data_entry_flow.AbortFlow("already_configured") + + async def async_set_unique_id( + self, unique_id: str | None = None, *, raise_on_progress: bool = True + ) -> ConfigEntry | None: + """Set a unique ID for the config flow. + + Returns optionally existing config entry with same ID. + """ + if unique_id is None: + self.context["unique_id"] = None + return None + + if raise_on_progress: + for progress in self._async_in_progress(): + if progress["context"].get("unique_id") == unique_id: + raise data_entry_flow.AbortFlow("already_in_progress") + + self.context["unique_id"] = unique_id + + # Abort discoveries done using the default discovery unique id + if unique_id != DEFAULT_DISCOVERY_UNIQUE_ID: + for progress in self._async_in_progress(): + if progress["context"].get("unique_id") == DEFAULT_DISCOVERY_UNIQUE_ID: + self.hass.config_entries.flow.async_abort(progress["flow_id"]) + + for entry in self._async_current_entries(include_ignore=True): + if entry.unique_id == unique_id: + return entry + + return None + + @callback + def _set_confirm_only( + self, + ) -> None: + """Mark the config flow as only needing user confirmation to finish flow.""" + self.context["confirm_only"] = True + + @callback + def _async_current_entries( + self, include_ignore: bool | None = None + ) -> list[ConfigEntry]: + """Return current entries. + + If the flow is user initiated, filter out ignored entries unless include_ignore is True. + """ + config_entries = self.hass.config_entries.async_entries(self.handler) + + if ( + include_ignore is True + or include_ignore is None + and self.source != SOURCE_USER + ): + return config_entries + + return [entry for entry in config_entries if entry.source != SOURCE_IGNORE] + + @callback + def _async_current_ids(self, include_ignore: bool = True) -> set[str | None]: + """Return current unique IDs.""" + return { + entry.unique_id + for entry in self.hass.config_entries.async_entries(self.handler) + if include_ignore or entry.source != SOURCE_IGNORE + } + + @callback + def _async_in_progress( + self, include_uninitialized: bool = False + ) -> list[data_entry_flow.FlowResult]: + """Return other in progress flows for current domain.""" + return [ + flw + for flw in self.hass.config_entries.flow.async_progress( + include_uninitialized=include_uninitialized + ) + if flw["handler"] == self.handler and flw["flow_id"] != self.flow_id + ] + + async def async_step_ignore( + self, user_input: dict[str, Any] + ) -> data_entry_flow.FlowResult: + """Ignore this config flow.""" + await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False) + return self.async_create_entry(title=user_input["title"], data={}) + + async def async_step_unignore( + self, user_input: dict[str, Any] + ) -> data_entry_flow.FlowResult: + """Rediscover a config entry by it's unique_id.""" + return self.async_abort(reason="not_implemented") + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: + """Handle a flow initiated by the user.""" + return self.async_abort(reason="not_implemented") + + async def _async_handle_discovery_without_unique_id(self) -> None: + """Mark this flow discovered, without a unique identifier. + + If a flow initiated by discovery, doesn't have a unique ID, this can + be used alternatively. It will ensure only 1 flow is started and only + when the handler has no existing config entries. + + It ensures that the discovery can be ignored by the user. + """ + if self.unique_id is not None: + return + + # Abort if the handler has config entries already + if self._async_current_entries(): + raise data_entry_flow.AbortFlow("already_configured") + + # Use an special unique id to differentiate + await self.async_set_unique_id(DEFAULT_DISCOVERY_UNIQUE_ID) + self._abort_if_unique_id_configured() + + # Abort if any other flow for this handler is already in progress + if self._async_in_progress(include_uninitialized=True): + raise data_entry_flow.AbortFlow("already_in_progress") + + async def async_step_discovery( + self, discovery_info: DiscoveryInfoType + ) -> data_entry_flow.FlowResult: + """Handle a flow initialized by discovery.""" + await self._async_handle_discovery_without_unique_id() + return await self.async_step_user() + + @callback + def async_abort( + self, *, reason: str, description_placeholders: dict | None = None + ) -> data_entry_flow.FlowResult: + """Abort the config flow.""" + # Remove reauth notification if no reauth flows are in progress + if self.source == SOURCE_REAUTH and not any( + ent["context"]["source"] == SOURCE_REAUTH + for ent in self.hass.config_entries.flow.async_progress() + if ent["flow_id"] != self.flow_id + ): + self.hass.components.persistent_notification.async_dismiss( + RECONFIGURE_NOTIFICATION_ID + ) + + return super().async_abort( + reason=reason, description_placeholders=description_placeholders + ) + + async def async_step_hassio( + self, discovery_info: DiscoveryInfoType + ) -> data_entry_flow.FlowResult: + """Handle a flow initialized by HASS IO discovery.""" + return await self.async_step_discovery(discovery_info) + + async def async_step_homekit( + self, discovery_info: DiscoveryInfoType + ) -> data_entry_flow.FlowResult: + """Handle a flow initialized by Homekit discovery.""" + return await self.async_step_discovery(discovery_info) + + async def async_step_mqtt( + self, discovery_info: DiscoveryInfoType + ) -> data_entry_flow.FlowResult: + """Handle a flow initialized by MQTT discovery.""" + return await self.async_step_discovery(discovery_info) + + async def async_step_ssdp( + self, discovery_info: DiscoveryInfoType + ) -> data_entry_flow.FlowResult: + """Handle a flow initialized by SSDP discovery.""" + return await self.async_step_discovery(discovery_info) + + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> data_entry_flow.FlowResult: + """Handle a flow initialized by Zeroconf discovery.""" + return await self.async_step_discovery(discovery_info) + + async def async_step_dhcp( + self, discovery_info: DiscoveryInfoType + ) -> data_entry_flow.FlowResult: + """Handle a flow initialized by DHCP discovery.""" + return await self.async_step_discovery(discovery_info) + + @callback + def async_create_entry( # pylint: disable=arguments-differ + self, + *, + title: str, + data: Mapping[str, Any], + description: str | None = None, + description_placeholders: dict | None = None, + options: Mapping[str, Any] | None = None, + ) -> data_entry_flow.FlowResult: + """Finish config flow and create a config entry.""" + result = super().async_create_entry( + title=title, + data=data, + description=description, + description_placeholders=description_placeholders, + ) + + result["options"] = options or {} + + return result + + +class OptionsFlowManager(data_entry_flow.FlowManager): + """Flow to set options for a configuration entry.""" + + async def async_create_flow( + self, + handler_key: Any, + *, + context: dict[str, Any] | None = None, + data: dict[str, Any] | None = None, + ) -> OptionsFlow: + """Create an options flow for a config entry. + + Entry_id and flow.handler is the same thing to map entry with flow. + """ + entry = self.hass.config_entries.async_get_entry(handler_key) + if entry is None: + raise UnknownEntry(handler_key) + + if entry.domain not in HANDLERS: + raise data_entry_flow.UnknownHandler + + return cast(OptionsFlow, HANDLERS[entry.domain].async_get_options_flow(entry)) + + async def async_finish_flow( + self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult + ) -> data_entry_flow.FlowResult: + """Finish an options flow and update options for configuration entry. + + Flow.handler and entry_id is the same thing to map flow with entry. + """ + flow = cast(OptionsFlow, flow) + + if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return result + + entry = self.hass.config_entries.async_get_entry(flow.handler) + if entry is None: + raise UnknownEntry(flow.handler) + if result["data"] is not None: + self.hass.config_entries.async_update_entry(entry, options=result["data"]) + + result["result"] = True + return result + + +class OptionsFlow(data_entry_flow.FlowHandler): + """Base class for config option flows.""" + + handler: str + + +@attr.s(slots=True) +class SystemOptions: + """Config entry system options.""" + + disable_new_entities: bool = attr.ib(default=False) + + def update(self, *, disable_new_entities: bool) -> None: + """Update properties.""" + self.disable_new_entities = disable_new_entities + + def as_dict(self) -> dict[str, Any]: + """Return dictionary version of this config entries system options.""" + return {"disable_new_entities": self.disable_new_entities} + + +class EntityRegistryDisabledHandler: + """Handler to handle when entities related to config entries updating disabled_by.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the handler.""" + self.hass = hass + self.registry: entity_registry.EntityRegistry | None = None + self.changed: set[str] = set() + self._remove_call_later: Callable[[], None] | None = None + + @callback + def async_setup(self) -> None: + """Set up the disable handler.""" + self.hass.bus.async_listen( + entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, + self._handle_entry_updated, + event_filter=_handle_entry_updated_filter, + ) + + async def _handle_entry_updated(self, event: Event) -> None: + """Handle entity registry entry update.""" + if self.registry is None: + self.registry = await entity_registry.async_get_registry(self.hass) + + entity_entry = self.registry.async_get(event.data["entity_id"]) + + if ( + # Stop if no entry found + entity_entry is None + # Stop if entry not connected to config entry + or entity_entry.config_entry_id is None + # Stop if the entry got disabled. In that case the entity handles it + # themselves. + or entity_entry.disabled_by + ): + return + + config_entry = self.hass.config_entries.async_get_entry( + entity_entry.config_entry_id + ) + assert config_entry is not None + + if config_entry.entry_id not in self.changed and config_entry.supports_unload: + self.changed.add(config_entry.entry_id) + + if not self.changed: + return + + # We are going to delay reloading on *every* entity registry change so that + # if a user is happily clicking along, it will only reload at the end. + + if self._remove_call_later: + self._remove_call_later() + + self._remove_call_later = self.hass.helpers.event.async_call_later( + RELOAD_AFTER_UPDATE_DELAY, self._handle_reload + ) + + async def _handle_reload(self, _now: Any) -> None: + """Handle a reload.""" + self._remove_call_later = None + to_reload = self.changed + self.changed = set() + + _LOGGER.info( + "Reloading configuration entries because disabled_by changed in entity registry: %s", + ", ".join(self.changed), + ) + + await asyncio.gather( + *[self.hass.config_entries.async_reload(entry_id) for entry_id in to_reload] + ) + + +@callback +def _handle_entry_updated_filter(event: Event) -> bool: + """Handle entity registry entry update filter. + + Only handle changes to "disabled_by". + If "disabled_by" was DISABLED_CONFIG_ENTRY, reload is not needed. + """ + if ( + event.data["action"] != "update" + or "disabled_by" not in event.data["changes"] + or event.data["changes"]["disabled_by"] == entity_registry.DISABLED_CONFIG_ENTRY + ): + return False + return True + + +async def support_entry_unload(hass: HomeAssistant, domain: str) -> bool: + """Test if a domain supports entry unloading.""" + integration = await loader.async_get_integration(hass, domain) + component = integration.get_component() + return hasattr(component, "async_unload_entry") diff --git a/homeassistant-2021.6.0.dev0/homeassistant/const.py b/homeassistant-2021.6.0.dev0/homeassistant/const.py new file mode 100644 index 00000000000..8b965995754 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/const.py @@ -0,0 +1,663 @@ +"""Constants used by Home Assistant components.""" +from typing import Final + +MAJOR_VERSION = 2021 +MINOR_VERSION = 6 +PATCH_VERSION = "0.dev0" +__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" +__version__ = f"{__short_version__}.{PATCH_VERSION}" +REQUIRED_PYTHON_VER = (3, 8, 0) +# Truthy date string triggers showing related deprecation warning messages. +REQUIRED_NEXT_PYTHON_VER = (3, 9, 0) +REQUIRED_NEXT_PYTHON_DATE = "" + +# Format for platform files +PLATFORM_FORMAT = "{platform}.{domain}" + +# Can be used to specify a catch all when registering state or event listeners. +MATCH_ALL = "*" + +# Entity target all constant +ENTITY_MATCH_NONE = "none" +ENTITY_MATCH_ALL = "all" + +# If no name is specified +DEVICE_DEFAULT_NAME = "Unnamed Device" + +# Max characters for an event_type (changing this requires a recorder +# database migration) +MAX_LENGTH_EVENT_TYPE = 64 + +# Sun events +SUN_EVENT_SUNSET = "sunset" +SUN_EVENT_SUNRISE = "sunrise" + +# #### CONFIG #### +CONF_ABOVE = "above" +CONF_ACCESS_TOKEN = "access_token" +CONF_ADDRESS = "address" +CONF_AFTER = "after" +CONF_ALIAS = "alias" +CONF_ALLOWLIST_EXTERNAL_URLS = "allowlist_external_urls" +CONF_API_KEY = "api_key" +CONF_API_TOKEN = "api_token" +CONF_API_VERSION = "api_version" +CONF_ARMING_TIME = "arming_time" +CONF_AT = "at" +CONF_ATTRIBUTE = "attribute" +CONF_AUTH_MFA_MODULES = "auth_mfa_modules" +CONF_AUTH_PROVIDERS = "auth_providers" +CONF_AUTHENTICATION = "authentication" +CONF_BASE = "base" +CONF_BEFORE = "before" +CONF_BELOW = "below" +CONF_BINARY_SENSORS = "binary_sensors" +CONF_BRIGHTNESS = "brightness" +CONF_BROADCAST_ADDRESS = "broadcast_address" +CONF_BROADCAST_PORT = "broadcast_port" +CONF_CHOOSE = "choose" +CONF_CLIENT_ID = "client_id" +CONF_CLIENT_SECRET = "client_secret" +CONF_CODE = "code" +CONF_COLOR_TEMP = "color_temp" +CONF_COMMAND = "command" +CONF_COMMAND_CLOSE = "command_close" +CONF_COMMAND_OFF = "command_off" +CONF_COMMAND_ON = "command_on" +CONF_COMMAND_OPEN = "command_open" +CONF_COMMAND_STATE = "command_state" +CONF_COMMAND_STOP = "command_stop" +CONF_CONDITION = "condition" +CONF_CONDITIONS = "conditions" +CONF_CONTINUE_ON_TIMEOUT = "continue_on_timeout" +CONF_COUNT = "count" +CONF_COVERS = "covers" +CONF_CURRENCY = "currency" +CONF_CUSTOMIZE = "customize" +CONF_CUSTOMIZE_DOMAIN = "customize_domain" +CONF_CUSTOMIZE_GLOB = "customize_glob" +CONF_DEFAULT = "default" +CONF_DELAY = "delay" +CONF_DELAY_TIME = "delay_time" +CONF_DESCRIPTION = "description" +CONF_DEVICE = "device" +CONF_DEVICES = "devices" +CONF_DEVICE_CLASS = "device_class" +CONF_DEVICE_ID = "device_id" +CONF_DISARM_AFTER_TRIGGER = "disarm_after_trigger" +CONF_DISCOVERY = "discovery" +CONF_DISKS = "disks" +CONF_DISPLAY_CURRENCY = "display_currency" +CONF_DISPLAY_OPTIONS = "display_options" +CONF_DOMAIN = "domain" +CONF_DOMAINS = "domains" +CONF_EFFECT = "effect" +CONF_ELEVATION = "elevation" +CONF_EMAIL = "email" +CONF_ENTITIES = "entities" +CONF_ENTITY_ID = "entity_id" +CONF_ENTITY_NAMESPACE = "entity_namespace" +CONF_ENTITY_PICTURE_TEMPLATE = "entity_picture_template" +CONF_EVENT = "event" +CONF_EVENT_DATA = "event_data" +CONF_EVENT_DATA_TEMPLATE = "event_data_template" +CONF_EXCLUDE = "exclude" +CONF_EXTERNAL_URL = "external_url" +CONF_FILENAME = "filename" +CONF_FILE_PATH = "file_path" +CONF_FOR = "for" +CONF_FORCE_UPDATE = "force_update" +CONF_FRIENDLY_NAME = "friendly_name" +CONF_FRIENDLY_NAME_TEMPLATE = "friendly_name_template" +CONF_HEADERS = "headers" +CONF_HOST = "host" +CONF_HOSTS = "hosts" +CONF_HS = "hs" +CONF_ICON = "icon" +CONF_ICON_TEMPLATE = "icon_template" +CONF_ID = "id" +CONF_INCLUDE = "include" +CONF_INTERNAL_URL = "internal_url" +CONF_IP_ADDRESS = "ip_address" +CONF_LATITUDE = "latitude" +CONF_LEGACY_TEMPLATES = "legacy_templates" +CONF_LIGHTS = "lights" +CONF_LONGITUDE = "longitude" +CONF_MAC = "mac" +CONF_MAXIMUM = "maximum" +CONF_MEDIA_DIRS = "media_dirs" +CONF_METHOD = "method" +CONF_MINIMUM = "minimum" +CONF_MODE = "mode" +CONF_MONITORED_CONDITIONS = "monitored_conditions" +CONF_MONITORED_VARIABLES = "monitored_variables" +CONF_NAME = "name" +CONF_OFFSET = "offset" +CONF_OPTIMISTIC = "optimistic" +CONF_PACKAGES = "packages" +CONF_PARAMS = "params" +CONF_PASSWORD = "password" +CONF_PATH = "path" +CONF_PAYLOAD = "payload" +CONF_PAYLOAD_OFF = "payload_off" +CONF_PAYLOAD_ON = "payload_on" +CONF_PENDING_TIME = "pending_time" +CONF_PIN = "pin" +CONF_PLATFORM = "platform" +CONF_PORT = "port" +CONF_PREFIX = "prefix" +CONF_PROFILE_NAME = "profile_name" +CONF_PROTOCOL = "protocol" +CONF_PROXY_SSL = "proxy_ssl" +CONF_QUOTE = "quote" +CONF_RADIUS = "radius" +CONF_RECIPIENT = "recipient" +CONF_REGION = "region" +CONF_REPEAT = "repeat" +CONF_RESOURCE = "resource" +CONF_RESOURCES = "resources" +CONF_RESOURCE_TEMPLATE = "resource_template" +CONF_RGB = "rgb" +CONF_ROOM = "room" +CONF_SCAN_INTERVAL = "scan_interval" +CONF_SCENE = "scene" +CONF_SELECTOR = "selector" +CONF_SENDER = "sender" +CONF_SENSORS = "sensors" +CONF_SENSOR_TYPE = "sensor_type" +CONF_SEQUENCE = "sequence" +CONF_SERVICE = "service" +CONF_SERVICE_DATA = "data" +CONF_SERVICE_TEMPLATE = "service_template" +CONF_SHOW_ON_MAP = "show_on_map" +CONF_SLAVE = "slave" +CONF_SOURCE = "source" +CONF_SSL = "ssl" +CONF_STATE = "state" +CONF_STATE_TEMPLATE = "state_template" +CONF_STRUCTURE = "structure" +CONF_SWITCHES = "switches" +CONF_TARGET = "target" +CONF_TEMPERATURE_UNIT = "temperature_unit" +CONF_TIMEOUT = "timeout" +CONF_TIME_ZONE = "time_zone" +CONF_TOKEN = "token" +CONF_TRIGGER_TIME = "trigger_time" +CONF_TTL = "ttl" +CONF_TYPE = "type" +CONF_UNIQUE_ID = "unique_id" +CONF_UNIT_OF_MEASUREMENT = "unit_of_measurement" +CONF_UNIT_SYSTEM = "unit_system" +CONF_UNTIL = "until" +CONF_URL = "url" +CONF_USERNAME = "username" +CONF_VALUE_TEMPLATE = "value_template" +CONF_VARIABLES = "variables" +CONF_VERIFY_SSL = "verify_ssl" +CONF_WAIT_FOR_TRIGGER = "wait_for_trigger" +CONF_WAIT_TEMPLATE = "wait_template" +CONF_WEBHOOK_ID = "webhook_id" +CONF_WEEKDAY = "weekday" +CONF_WHILE = "while" +CONF_WHITELIST = "whitelist" +CONF_ALLOWLIST_EXTERNAL_DIRS = "allowlist_external_dirs" +LEGACY_CONF_WHITELIST_EXTERNAL_DIRS = "whitelist_external_dirs" +CONF_WHITE_VALUE = "white_value" +CONF_XY = "xy" +CONF_ZONE = "zone" + +# #### EVENTS #### +EVENT_CALL_SERVICE = "call_service" +EVENT_COMPONENT_LOADED = "component_loaded" +EVENT_CORE_CONFIG_UPDATE = "core_config_updated" +EVENT_HOMEASSISTANT_CLOSE = "homeassistant_close" +EVENT_HOMEASSISTANT_START = "homeassistant_start" +EVENT_HOMEASSISTANT_STARTED = "homeassistant_started" +EVENT_HOMEASSISTANT_STOP = "homeassistant_stop" +EVENT_HOMEASSISTANT_FINAL_WRITE = "homeassistant_final_write" +EVENT_LOGBOOK_ENTRY = "logbook_entry" +EVENT_SERVICE_REGISTERED = "service_registered" +EVENT_SERVICE_REMOVED = "service_removed" +EVENT_STATE_CHANGED = "state_changed" +EVENT_THEMES_UPDATED = "themes_updated" +EVENT_TIMER_OUT_OF_SYNC = "timer_out_of_sync" +EVENT_TIME_CHANGED = "time_changed" + + +# #### DEVICE CLASSES #### +DEVICE_CLASS_BATTERY = "battery" +DEVICE_CLASS_CO = "carbon_monoxide" +DEVICE_CLASS_CO2 = "carbon_dioxide" +DEVICE_CLASS_HUMIDITY = "humidity" +DEVICE_CLASS_ILLUMINANCE = "illuminance" +DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength" +DEVICE_CLASS_TEMPERATURE = "temperature" +DEVICE_CLASS_TIMESTAMP = "timestamp" +DEVICE_CLASS_PRESSURE = "pressure" +DEVICE_CLASS_POWER = "power" +DEVICE_CLASS_CURRENT = "current" +DEVICE_CLASS_ENERGY = "energy" +DEVICE_CLASS_POWER_FACTOR = "power_factor" +DEVICE_CLASS_VOLTAGE = "voltage" + +# #### STATES #### +STATE_ON = "on" +STATE_OFF = "off" +STATE_HOME = "home" +STATE_NOT_HOME = "not_home" +STATE_UNKNOWN = "unknown" +STATE_OPEN = "open" +STATE_OPENING = "opening" +STATE_CLOSED = "closed" +STATE_CLOSING = "closing" +STATE_PLAYING = "playing" +STATE_PAUSED = "paused" +STATE_IDLE = "idle" +STATE_STANDBY = "standby" +STATE_ALARM_DISARMED = "disarmed" +STATE_ALARM_ARMED_HOME = "armed_home" +STATE_ALARM_ARMED_AWAY = "armed_away" +STATE_ALARM_ARMED_NIGHT = "armed_night" +STATE_ALARM_ARMED_CUSTOM_BYPASS = "armed_custom_bypass" +STATE_ALARM_PENDING = "pending" +STATE_ALARM_ARMING = "arming" +STATE_ALARM_DISARMING = "disarming" +STATE_ALARM_TRIGGERED = "triggered" +STATE_LOCKED = "locked" +STATE_UNLOCKED = "unlocked" +STATE_UNAVAILABLE = "unavailable" +STATE_OK = "ok" +STATE_PROBLEM = "problem" + +# #### STATE AND EVENT ATTRIBUTES #### +# Attribution +ATTR_ATTRIBUTION = "attribution" + +# Credentials +ATTR_CREDENTIALS = "credentials" + +# Contains time-related attributes +ATTR_NOW = "now" +ATTR_DATE = "date" +ATTR_TIME = "time" +ATTR_SECONDS = "seconds" + +# Contains domain, service for a SERVICE_CALL event +ATTR_DOMAIN = "domain" +ATTR_SERVICE = "service" +ATTR_SERVICE_DATA = "service_data" + +# IDs +ATTR_ID = "id" + +# Name +ATTR_NAME: Final = "name" + +# Contains one string or a list of strings, each being an entity id +ATTR_ENTITY_ID: Final = "entity_id" + +# Contains one string or a list of strings, each being an area id +ATTR_AREA_ID = "area_id" + +# Contains one string, the device ID +ATTR_DEVICE_ID = "device_id" + +# String with a friendly name for the entity +ATTR_FRIENDLY_NAME = "friendly_name" + +# A picture to represent entity +ATTR_ENTITY_PICTURE = "entity_picture" + +ATTR_IDENTIFIERS: Final = "identifiers" + +# Icon to use in the frontend +ATTR_ICON = "icon" + +# The unit of measurement if applicable +ATTR_UNIT_OF_MEASUREMENT: Final = "unit_of_measurement" + +CONF_UNIT_SYSTEM_METRIC: str = "metric" +CONF_UNIT_SYSTEM_IMPERIAL: str = "imperial" + +# Electrical attributes +ATTR_VOLTAGE = "voltage" + +# Location of the device/sensor +ATTR_LOCATION = "location" + +ATTR_MODE = "mode" + +ATTR_MANUFACTURER: Final = "manufacturer" +ATTR_MODEL: Final = "model" +ATTR_SW_VERSION: Final = "sw_version" + +ATTR_BATTERY_CHARGING = "battery_charging" +ATTR_BATTERY_LEVEL: Final = "battery_level" +ATTR_WAKEUP = "wake_up_interval" + +# For devices which support a code attribute +ATTR_CODE = "code" +ATTR_CODE_FORMAT = "code_format" + +# For calling a device specific command +ATTR_COMMAND = "command" + +# For devices which support an armed state +ATTR_ARMED = "device_armed" + +# For devices which support a locked state +ATTR_LOCKED = "locked" + +# For sensors that support 'tripping', eg. motion and door sensors +ATTR_TRIPPED = "device_tripped" + +# For sensors that support 'tripping' this holds the most recent +# time the device was tripped +ATTR_LAST_TRIP_TIME = "last_tripped_time" + +# For all entity's, this hold whether or not it should be hidden +ATTR_HIDDEN = "hidden" + +# Location of the entity +ATTR_LATITUDE = "latitude" +ATTR_LONGITUDE = "longitude" + +# Accuracy of location in meters +ATTR_GPS_ACCURACY = "gps_accuracy" + +# If state is assumed +ATTR_ASSUMED_STATE = "assumed_state" +ATTR_STATE = "state" + +ATTR_EDITABLE = "editable" +ATTR_OPTION = "option" + +# The entity has been restored with restore state +ATTR_RESTORED = "restored" + +# Bitfield of supported component features for the entity +ATTR_SUPPORTED_FEATURES = "supported_features" + +# Class of device within its domain +ATTR_DEVICE_CLASS: Final = "device_class" + +# Temperature attribute +ATTR_TEMPERATURE: Final = "temperature" + +# #### UNITS OF MEASUREMENT #### +# Power units +POWER_WATT = "W" +POWER_KILO_WATT = "kW" + +# Voltage units +VOLT = "V" + +# Energy units +ENERGY_WATT_HOUR = "Wh" +ENERGY_KILO_WATT_HOUR = "kWh" + +# Electrical units +ELECTRICAL_CURRENT_AMPERE = "A" +ELECTRICAL_VOLT_AMPERE = "VA" + +# Degree units +DEGREE = "°" + +# Currency units +CURRENCY_EURO = "€" +CURRENCY_DOLLAR = "$" +CURRENCY_CENT = "¢" + +# Temperature units +TEMP_CELSIUS = "°C" +TEMP_FAHRENHEIT = "°F" +TEMP_KELVIN = "K" + +# Time units +TIME_MICROSECONDS = "μs" +TIME_MILLISECONDS = "ms" +TIME_SECONDS = "s" +TIME_MINUTES = "min" +TIME_HOURS = "h" +TIME_DAYS = "d" +TIME_WEEKS = "w" +TIME_MONTHS = "m" +TIME_YEARS = "y" + +# Length units +LENGTH_MILLIMETERS: str = "mm" +LENGTH_CENTIMETERS: str = "cm" +LENGTH_METERS: str = "m" +LENGTH_KILOMETERS: str = "km" + +LENGTH_INCHES: str = "in" +LENGTH_FEET: str = "ft" +LENGTH_YARD: str = "yd" +LENGTH_MILES: str = "mi" + +# Frequency units +FREQUENCY_HERTZ = "Hz" +FREQUENCY_GIGAHERTZ = "GHz" + +# Pressure units +PRESSURE_PA: str = "Pa" +PRESSURE_HPA: str = "hPa" +PRESSURE_BAR: str = "bar" +PRESSURE_MBAR: str = "mbar" +PRESSURE_INHG: str = "inHg" +PRESSURE_PSI: str = "psi" + +# Volume units +VOLUME_LITERS: str = "L" +VOLUME_MILLILITERS: str = "mL" +VOLUME_CUBIC_METERS = "m³" +VOLUME_CUBIC_FEET = "ft³" + +VOLUME_GALLONS: str = "gal" +VOLUME_FLUID_OUNCE: str = "fl. oz." + +# Volume Flow Rate units +VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR = "m³/h" +VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE = "ft³/m" + +# Area units +AREA_SQUARE_METERS = "m²" + +# Mass units +MASS_GRAMS: str = "g" +MASS_KILOGRAMS: str = "kg" +MASS_MILLIGRAMS = "mg" +MASS_MICROGRAMS = "µg" + +MASS_OUNCES: str = "oz" +MASS_POUNDS: str = "lb" + +# Conductivity units +CONDUCTIVITY: str = "µS/cm" + +# Light units +LIGHT_LUX: str = "lx" + +# UV Index units +UV_INDEX: str = "UV index" + +# Percentage units +PERCENTAGE = "%" + +# Irradiation units +IRRADIATION_WATTS_PER_SQUARE_METER = "W/m²" + +# Precipitation units +PRECIPITATION_MILLIMETERS_PER_HOUR = "mm/h" + +# Concentration units +CONCENTRATION_MICROGRAMS_PER_CUBIC_METER = "µg/m³" +CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER = "mg/m³" +CONCENTRATION_PARTS_PER_CUBIC_METER = "p/m³" +CONCENTRATION_PARTS_PER_MILLION = "ppm" +CONCENTRATION_PARTS_PER_BILLION = "ppb" + +# Speed units +SPEED_MILLIMETERS_PER_DAY = "mm/d" +SPEED_INCHES_PER_DAY = "in/d" +SPEED_METERS_PER_SECOND = "m/s" +SPEED_INCHES_PER_HOUR = "in/h" +SPEED_KILOMETERS_PER_HOUR = "km/h" +SPEED_MILES_PER_HOUR = "mph" + +# Signal_strength units +SIGNAL_STRENGTH_DECIBELS = "dB" +SIGNAL_STRENGTH_DECIBELS_MILLIWATT = "dBm" + +# Data units +DATA_BITS = "bit" +DATA_KILOBITS = "kbit" +DATA_MEGABITS = "Mbit" +DATA_GIGABITS = "Gbit" +DATA_BYTES = "B" +DATA_KILOBYTES = "kB" +DATA_MEGABYTES = "MB" +DATA_GIGABYTES = "GB" +DATA_TERABYTES = "TB" +DATA_PETABYTES = "PB" +DATA_EXABYTES = "EB" +DATA_ZETTABYTES = "ZB" +DATA_YOTTABYTES = "YB" +DATA_KIBIBYTES = "KiB" +DATA_MEBIBYTES = "MiB" +DATA_GIBIBYTES = "GiB" +DATA_TEBIBYTES = "TiB" +DATA_PEBIBYTES = "PiB" +DATA_EXBIBYTES = "EiB" +DATA_ZEBIBYTES = "ZiB" +DATA_YOBIBYTES = "YiB" +DATA_RATE_BITS_PER_SECOND = "bit/s" +DATA_RATE_KILOBITS_PER_SECOND = "kbit/s" +DATA_RATE_MEGABITS_PER_SECOND = "Mbit/s" +DATA_RATE_GIGABITS_PER_SECOND = "Gbit/s" +DATA_RATE_BYTES_PER_SECOND = "B/s" +DATA_RATE_KILOBYTES_PER_SECOND = "kB/s" +DATA_RATE_MEGABYTES_PER_SECOND = "MB/s" +DATA_RATE_GIGABYTES_PER_SECOND = "GB/s" +DATA_RATE_KIBIBYTES_PER_SECOND = "KiB/s" +DATA_RATE_MEBIBYTES_PER_SECOND = "MiB/s" +DATA_RATE_GIBIBYTES_PER_SECOND = "GiB/s" + +# #### SERVICES #### +SERVICE_HOMEASSISTANT_STOP = "stop" +SERVICE_HOMEASSISTANT_RESTART = "restart" + +SERVICE_TURN_ON = "turn_on" +SERVICE_TURN_OFF = "turn_off" +SERVICE_TOGGLE = "toggle" +SERVICE_RELOAD = "reload" + +SERVICE_VOLUME_UP = "volume_up" +SERVICE_VOLUME_DOWN = "volume_down" +SERVICE_VOLUME_MUTE = "volume_mute" +SERVICE_VOLUME_SET = "volume_set" +SERVICE_MEDIA_PLAY_PAUSE = "media_play_pause" +SERVICE_MEDIA_PLAY = "media_play" +SERVICE_MEDIA_PAUSE = "media_pause" +SERVICE_MEDIA_STOP = "media_stop" +SERVICE_MEDIA_NEXT_TRACK = "media_next_track" +SERVICE_MEDIA_PREVIOUS_TRACK = "media_previous_track" +SERVICE_MEDIA_SEEK = "media_seek" +SERVICE_REPEAT_SET = "repeat_set" +SERVICE_SHUFFLE_SET = "shuffle_set" + +SERVICE_ALARM_DISARM = "alarm_disarm" +SERVICE_ALARM_ARM_HOME = "alarm_arm_home" +SERVICE_ALARM_ARM_AWAY = "alarm_arm_away" +SERVICE_ALARM_ARM_NIGHT = "alarm_arm_night" +SERVICE_ALARM_ARM_CUSTOM_BYPASS = "alarm_arm_custom_bypass" +SERVICE_ALARM_TRIGGER = "alarm_trigger" + + +SERVICE_LOCK = "lock" +SERVICE_UNLOCK = "unlock" + +SERVICE_OPEN = "open" +SERVICE_CLOSE = "close" + +SERVICE_CLOSE_COVER = "close_cover" +SERVICE_CLOSE_COVER_TILT = "close_cover_tilt" +SERVICE_OPEN_COVER = "open_cover" +SERVICE_OPEN_COVER_TILT = "open_cover_tilt" +SERVICE_SET_COVER_POSITION = "set_cover_position" +SERVICE_SET_COVER_TILT_POSITION = "set_cover_tilt_position" +SERVICE_STOP_COVER = "stop_cover" +SERVICE_STOP_COVER_TILT = "stop_cover_tilt" +SERVICE_TOGGLE_COVER_TILT = "toggle_cover_tilt" + +SERVICE_SELECT_OPTION = "select_option" + +# #### API / REMOTE #### +SERVER_PORT: Final = 8123 + +URL_ROOT = "/" +URL_API = "/api/" +URL_API_STREAM = "/api/stream" +URL_API_CONFIG = "/api/config" +URL_API_DISCOVERY_INFO = "/api/discovery_info" +URL_API_STATES = "/api/states" +URL_API_STATES_ENTITY = "/api/states/{}" +URL_API_EVENTS = "/api/events" +URL_API_EVENTS_EVENT = "/api/events/{}" +URL_API_SERVICES = "/api/services" +URL_API_SERVICES_SERVICE = "/api/services/{}/{}" +URL_API_COMPONENTS = "/api/components" +URL_API_ERROR_LOG = "/api/error_log" +URL_API_LOG_OUT = "/api/log_out" +URL_API_TEMPLATE = "/api/template" + +HTTP_OK = 200 +HTTP_CREATED = 201 +HTTP_ACCEPTED = 202 +HTTP_MOVED_PERMANENTLY = 301 +HTTP_BAD_REQUEST = 400 +HTTP_UNAUTHORIZED = 401 +HTTP_FORBIDDEN = 403 +HTTP_NOT_FOUND = 404 +HTTP_METHOD_NOT_ALLOWED = 405 +HTTP_UNPROCESSABLE_ENTITY = 422 +HTTP_TOO_MANY_REQUESTS = 429 +HTTP_INTERNAL_SERVER_ERROR = 500 +HTTP_BAD_GATEWAY = 502 +HTTP_SERVICE_UNAVAILABLE = 503 + +HTTP_BASIC_AUTHENTICATION = "basic" +HTTP_DIGEST_AUTHENTICATION = "digest" + +HTTP_HEADER_X_REQUESTED_WITH = "X-Requested-With" + +CONTENT_TYPE_JSON = "application/json" +CONTENT_TYPE_MULTIPART = "multipart/x-mixed-replace; boundary={}" +CONTENT_TYPE_TEXT_PLAIN = "text/plain" + +# The exit code to send to request a restart +RESTART_EXIT_CODE = 100 + +UNIT_NOT_RECOGNIZED_TEMPLATE: str = "{} is not a recognized {} unit." + +LENGTH: str = "length" +MASS: str = "mass" +PRESSURE: str = "pressure" +VOLUME: str = "volume" +TEMPERATURE: str = "temperature" +SPEED_MS: str = "speed_ms" +ILLUMINANCE: str = "illuminance" + +WEEKDAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] + +# The degree of precision for platforms +PRECISION_WHOLE = 1 +PRECISION_HALVES = 0.5 +PRECISION_TENTHS = 0.1 + +# Static list of entities that will never be exposed to +# cloud, alexa, or google_home components +CLOUD_NEVER_EXPOSED_ENTITIES = ["group.all_locks"] + +# The ID of the Home Assistant Cast App +CAST_APP_ID_HOMEASSISTANT = "B12CE3CA" diff --git a/homeassistant-2021.6.0.dev0/homeassistant/core.py b/homeassistant-2021.6.0.dev0/homeassistant/core.py new file mode 100644 index 00000000000..b1610faad6e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/core.py @@ -0,0 +1,1787 @@ +""" +Core components of Home Assistant. + +Home Assistant is a Home Automation framework for observing the state +of entities and react to changes. +""" +from __future__ import annotations + +import asyncio +from collections.abc import Awaitable, Collection, Coroutine, Iterable, Mapping +import datetime +import enum +import functools +import logging +import os +import pathlib +import re +import threading +from time import monotonic +from types import MappingProxyType +from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, cast + +import attr +import voluptuous as vol +import yarl + +from homeassistant import block_async_io, loader, util +from homeassistant.const import ( + ATTR_DOMAIN, + ATTR_FRIENDLY_NAME, + ATTR_NOW, + ATTR_SECONDS, + ATTR_SERVICE, + ATTR_SERVICE_DATA, + CONF_UNIT_SYSTEM_IMPERIAL, + EVENT_CALL_SERVICE, + EVENT_CORE_CONFIG_UPDATE, + EVENT_HOMEASSISTANT_CLOSE, + EVENT_HOMEASSISTANT_FINAL_WRITE, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, + EVENT_SERVICE_REGISTERED, + EVENT_SERVICE_REMOVED, + EVENT_STATE_CHANGED, + EVENT_TIME_CHANGED, + EVENT_TIMER_OUT_OF_SYNC, + LENGTH_METERS, + MATCH_ALL, + MAX_LENGTH_EVENT_TYPE, + __version__, +) +from homeassistant.exceptions import ( + HomeAssistantError, + InvalidEntityFormatError, + InvalidStateError, + MaxLengthExceeded, + ServiceNotFound, + Unauthorized, +) +from homeassistant.util import location +from homeassistant.util.async_ import ( + fire_coroutine_threadsafe, + run_callback_threadsafe, + shutdown_run_callback_threadsafe, +) +import homeassistant.util.dt as dt_util +from homeassistant.util.timeout import TimeoutManager +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem +import homeassistant.util.uuid as uuid_util + +# Typing imports that create a circular dependency +if TYPE_CHECKING: + from homeassistant.auth import AuthManager + from homeassistant.components.http import HomeAssistantHTTP + from homeassistant.config_entries import ConfigEntries + + +STAGE_1_SHUTDOWN_TIMEOUT = 100 +STAGE_2_SHUTDOWN_TIMEOUT = 60 +STAGE_3_SHUTDOWN_TIMEOUT = 30 + + +block_async_io.enable() + +T = TypeVar("T") +_UNDEF: dict = {} # Internal; not helpers.typing.UNDEFINED due to circular dependency +# pylint: disable=invalid-name +CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) +CALLBACK_TYPE = Callable[[], None] +# pylint: enable=invalid-name + +CORE_STORAGE_KEY = "core.config" +CORE_STORAGE_VERSION = 1 + +DOMAIN = "homeassistant" + +# How long to wait to log tasks that are blocking +BLOCK_LOG_TIMEOUT = 60 + +# How long we wait for the result of a service call +SERVICE_CALL_LIMIT = 10 # seconds + +# Source of core configuration +SOURCE_DISCOVERED = "discovered" +SOURCE_STORAGE = "storage" +SOURCE_YAML = "yaml" + +# How long to wait until things that run on startup have to finish. +TIMEOUT_EVENT_START = 15 + +_LOGGER = logging.getLogger(__name__) + + +def split_entity_id(entity_id: str) -> list[str]: + """Split a state entity ID into domain and object ID.""" + return entity_id.split(".", 1) + + +VALID_ENTITY_ID = re.compile(r"^(?!.+__)(?!_)[\da-z_]+(? bool: + """Test if an entity ID is a valid format. + + Format: . where both are slugs. + """ + return VALID_ENTITY_ID.match(entity_id) is not None + + +def valid_state(state: str) -> bool: + """Test if a state is valid.""" + return len(state) < 256 + + +def callback(func: CALLABLE_T) -> CALLABLE_T: + """Annotation to mark method as safe to call from within the event loop.""" + setattr(func, "_hass_callback", True) + return func + + +def is_callback(func: Callable[..., Any]) -> bool: + """Check if function is safe to be called in the event loop.""" + return getattr(func, "_hass_callback", False) is True + + +@enum.unique +class HassJobType(enum.Enum): + """Represent a job type.""" + + Coroutinefunction = 1 + Callback = 2 + Executor = 3 + + +class HassJob: + """Represent a job to be run later. + + We check the callable type in advance + so we can avoid checking it every time + we run the job. + """ + + __slots__ = ("job_type", "target") + + def __init__(self, target: Callable): + """Create a job object.""" + if asyncio.iscoroutine(target): + raise ValueError("Coroutine not allowed to be passed to HassJob") + + self.target = target + self.job_type = _get_callable_job_type(target) + + def __repr__(self) -> str: + """Return the job.""" + return f"" + + +def _get_callable_job_type(target: Callable) -> HassJobType: + """Determine the job type from the callable.""" + # Check for partials to properly determine if coroutine function + check_target = target + while isinstance(check_target, functools.partial): + check_target = check_target.func + + if asyncio.iscoroutinefunction(check_target): + return HassJobType.Coroutinefunction + if is_callback(check_target): + return HassJobType.Callback + return HassJobType.Executor + + +class CoreState(enum.Enum): + """Represent the current state of Home Assistant.""" + + not_running = "NOT_RUNNING" + starting = "STARTING" + running = "RUNNING" + stopping = "STOPPING" + final_write = "FINAL_WRITE" + stopped = "STOPPED" + + def __str__(self) -> str: # pylint: disable=invalid-str-returned + """Return the event.""" + return self.value + + +class HomeAssistant: + """Root object of the Home Assistant home automation.""" + + auth: AuthManager + http: HomeAssistantHTTP = None # type: ignore + config_entries: ConfigEntries = None # type: ignore + + def __init__(self) -> None: + """Initialize new Home Assistant object.""" + self.loop = asyncio.get_running_loop() + self._pending_tasks: list = [] + self._track_task = True + self.bus = EventBus(self) + self.services = ServiceRegistry(self) + self.states = StateMachine(self.bus, self.loop) + self.config = Config(self) + self.components = loader.Components(self) + self.helpers = loader.Helpers(self) + # This is a dictionary that any component can store any data on. + self.data: dict = {} + self.state: CoreState = CoreState.not_running + self.exit_code: int = 0 + # If not None, use to signal end-of-loop + self._stopped: asyncio.Event | None = None + # Timeout handler for Core/Helper namespace + self.timeout: TimeoutManager = TimeoutManager() + + @property + def is_running(self) -> bool: + """Return if Home Assistant is running.""" + return self.state in (CoreState.starting, CoreState.running) + + @property + def is_stopping(self) -> bool: + """Return if Home Assistant is stopping.""" + return self.state in (CoreState.stopping, CoreState.final_write) + + def start(self) -> int: + """Start Home Assistant. + + Note: This function is only used for testing. + For regular use, use "await hass.run()". + """ + # Register the async start + fire_coroutine_threadsafe(self.async_start(), self.loop) + + # Run forever + # Block until stopped + _LOGGER.info("Starting Home Assistant core loop") + self.loop.run_forever() + return self.exit_code + + async def async_run(self, *, attach_signals: bool = True) -> int: + """Home Assistant main entry point. + + Start Home Assistant and block until stopped. + + This method is a coroutine. + """ + if self.state != CoreState.not_running: + raise RuntimeError("Home Assistant is already running") + + # _async_stop will set this instead of stopping the loop + self._stopped = asyncio.Event() + + await self.async_start() + if attach_signals: + # pylint: disable=import-outside-toplevel + from homeassistant.helpers.signal import async_register_signal_handling + + async_register_signal_handling(self) + + await self._stopped.wait() + return self.exit_code + + async def async_start(self) -> None: + """Finalize startup from inside the event loop. + + This method is a coroutine. + """ + _LOGGER.info("Starting Home Assistant") + setattr(self.loop, "_thread_ident", threading.get_ident()) + + self.state = CoreState.starting + self.bus.async_fire(EVENT_CORE_CONFIG_UPDATE) + self.bus.async_fire(EVENT_HOMEASSISTANT_START) + + try: + # Only block for EVENT_HOMEASSISTANT_START listener + self.async_stop_track_tasks() + async with self.timeout.async_timeout(TIMEOUT_EVENT_START): + await self.async_block_till_done() + except asyncio.TimeoutError: + _LOGGER.warning( + "Something is blocking Home Assistant from wrapping up the " + "start up phase. We're going to continue anyway. Please " + "report the following info at https://github.com/home-assistant/core/issues: %s", + ", ".join(self.config.components), + ) + + # Allow automations to set up the start triggers before changing state + await asyncio.sleep(0) + + if self.state != CoreState.starting: + _LOGGER.warning( + "Home Assistant startup has been interrupted. " + "Its state may be inconsistent" + ) + return + + self.state = CoreState.running + self.bus.async_fire(EVENT_CORE_CONFIG_UPDATE) + self.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + _async_create_timer(self) + + def add_job(self, target: Callable[..., Any], *args: Any) -> None: + """Add job to the executor pool. + + target: target to call. + args: parameters for method to call. + """ + if target is None: + raise ValueError("Don't call add_job with None") + self.loop.call_soon_threadsafe(self.async_add_job, target, *args) + + @callback + def async_add_job( + self, target: Callable[..., Any], *args: Any + ) -> asyncio.Future | None: + """Add a job from within the event loop. + + This method must be run in the event loop. + + target: target to call. + args: parameters for method to call. + """ + if target is None: + raise ValueError("Don't call async_add_job with None") + + if asyncio.iscoroutine(target): + return self.async_create_task(cast(Coroutine, target)) + + return self.async_add_hass_job(HassJob(target), *args) + + @callback + def async_add_hass_job(self, hassjob: HassJob, *args: Any) -> asyncio.Future | None: + """Add a HassJob from within the event loop. + + This method must be run in the event loop. + hassjob: HassJob to call. + args: parameters for method to call. + """ + if hassjob.job_type == HassJobType.Coroutinefunction: + task = self.loop.create_task(hassjob.target(*args)) + elif hassjob.job_type == HassJobType.Callback: + self.loop.call_soon(hassjob.target, *args) + return None + else: + task = self.loop.run_in_executor( # type: ignore + None, hassjob.target, *args + ) + + # If a task is scheduled + if self._track_task: + self._pending_tasks.append(task) + + return task + + def create_task(self, target: Coroutine) -> None: + """Add task to the executor pool. + + target: target to call. + """ + self.loop.call_soon_threadsafe(self.async_create_task, target) + + @callback + def async_create_task(self, target: Coroutine) -> asyncio.tasks.Task: + """Create a task from within the eventloop. + + This method must be run in the event loop. + + target: target to call. + """ + task: asyncio.tasks.Task = self.loop.create_task(target) + + if self._track_task: + self._pending_tasks.append(task) + + return task + + @callback + def async_add_executor_job( + self, target: Callable[..., T], *args: Any + ) -> Awaitable[T]: + """Add an executor job from within the event loop.""" + task = self.loop.run_in_executor(None, target, *args) + + # If a task is scheduled + if self._track_task: + self._pending_tasks.append(task) + + return task + + @callback + def async_track_tasks(self) -> None: + """Track tasks so you can wait for all tasks to be done.""" + self._track_task = True + + @callback + def async_stop_track_tasks(self) -> None: + """Stop track tasks so you can't wait for all tasks to be done.""" + self._track_task = False + + @callback + def async_run_hass_job(self, hassjob: HassJob, *args: Any) -> asyncio.Future | None: + """Run a HassJob from within the event loop. + + This method must be run in the event loop. + + hassjob: HassJob + args: parameters for method to call. + """ + if hassjob.job_type == HassJobType.Callback: + hassjob.target(*args) + return None + + return self.async_add_hass_job(hassjob, *args) + + @callback + def async_run_job( + self, target: Callable[..., None | Awaitable], *args: Any + ) -> asyncio.Future | None: + """Run a job from within the event loop. + + This method must be run in the event loop. + + target: target to call. + args: parameters for method to call. + """ + if asyncio.iscoroutine(target): + return self.async_create_task(cast(Coroutine, target)) + + return self.async_run_hass_job(HassJob(target), *args) + + def block_till_done(self) -> None: + """Block until all pending work is done.""" + asyncio.run_coroutine_threadsafe( + self.async_block_till_done(), self.loop + ).result() + + async def async_block_till_done(self) -> None: + """Block until all pending work is done.""" + # To flush out any call_soon_threadsafe + await asyncio.sleep(0) + start_time: float | None = None + + while self._pending_tasks: + pending = [task for task in self._pending_tasks if not task.done()] + self._pending_tasks.clear() + if pending: + await self._await_and_log_pending(pending) + + if start_time is None: + # Avoid calling monotonic() until we know + # we may need to start logging blocked tasks. + start_time = 0 + elif start_time == 0: + # If we have waited twice then we set the start + # time + start_time = monotonic() + elif monotonic() - start_time > BLOCK_LOG_TIMEOUT: + # We have waited at least three loops and new tasks + # continue to block. At this point we start + # logging all waiting tasks. + for task in pending: + _LOGGER.debug("Waiting for task: %s", task) + else: + await asyncio.sleep(0) + + async def _await_and_log_pending(self, pending: Iterable[Awaitable[Any]]) -> None: + """Await and log tasks that take a long time.""" + wait_time = 0 + while pending: + _, pending = await asyncio.wait(pending, timeout=BLOCK_LOG_TIMEOUT) + if not pending: + return + wait_time += BLOCK_LOG_TIMEOUT + for task in pending: + _LOGGER.debug("Waited %s seconds for task: %s", wait_time, task) + + def stop(self) -> None: + """Stop Home Assistant and shuts down all threads.""" + if self.state == CoreState.not_running: # just ignore + return + fire_coroutine_threadsafe(self.async_stop(), self.loop) + + async def async_stop(self, exit_code: int = 0, *, force: bool = False) -> None: + """Stop Home Assistant and shuts down all threads. + + The "force" flag commands async_stop to proceed regardless of + Home Assistan't current state. You should not set this flag + unless you're testing. + + This method is a coroutine. + """ + if not force: + # Some tests require async_stop to run, + # regardless of the state of the loop. + if self.state == CoreState.not_running: # just ignore + return + if self.state in [CoreState.stopping, CoreState.final_write]: + _LOGGER.info("Additional call to async_stop was ignored") + return + if self.state == CoreState.starting: + # This may not work + _LOGGER.warning( + "Stopping Home Assistant before startup has completed may fail" + ) + + # stage 1 + self.state = CoreState.stopping + self.async_track_tasks() + self.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + try: + async with self.timeout.async_timeout(STAGE_1_SHUTDOWN_TIMEOUT): + await self.async_block_till_done() + except asyncio.TimeoutError: + _LOGGER.warning( + "Timed out waiting for shutdown stage 1 to complete, the shutdown will continue" + ) + + # stage 2 + self.state = CoreState.final_write + self.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE) + try: + async with self.timeout.async_timeout(STAGE_2_SHUTDOWN_TIMEOUT): + await self.async_block_till_done() + except asyncio.TimeoutError: + _LOGGER.warning( + "Timed out waiting for shutdown stage 2 to complete, the shutdown will continue" + ) + + # stage 3 + self.state = CoreState.not_running + self.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE) + + # Prevent run_callback_threadsafe from scheduling any additional + # callbacks in the event loop as callbacks created on the futures + # it returns will never run after the final `self.async_block_till_done` + # which will cause the futures to block forever when waiting for + # the `result()` which will cause a deadlock when shutting down the executor. + shutdown_run_callback_threadsafe(self.loop) + + try: + async with self.timeout.async_timeout(STAGE_3_SHUTDOWN_TIMEOUT): + await self.async_block_till_done() + except asyncio.TimeoutError: + _LOGGER.warning( + "Timed out waiting for shutdown stage 3 to complete, the shutdown will continue" + ) + + self.exit_code = exit_code + self.state = CoreState.stopped + + if self._stopped is not None: + self._stopped.set() + + +@attr.s(slots=True, frozen=True) +class Context: + """The context that triggered something.""" + + user_id: str = attr.ib(default=None) + parent_id: str | None = attr.ib(default=None) + id: str = attr.ib(factory=uuid_util.random_uuid_hex) + + def as_dict(self) -> dict[str, str | None]: + """Return a dictionary representation of the context.""" + return {"id": self.id, "parent_id": self.parent_id, "user_id": self.user_id} + + +class EventOrigin(enum.Enum): + """Represent the origin of an event.""" + + local = "LOCAL" + remote = "REMOTE" + + def __str__(self) -> str: # pylint: disable=invalid-str-returned + """Return the event.""" + return self.value + + +class Event: + """Representation of an event within the bus.""" + + __slots__ = ["event_type", "data", "origin", "time_fired", "context"] + + def __init__( + self, + event_type: str, + data: dict[str, Any] | None = None, + origin: EventOrigin = EventOrigin.local, + time_fired: datetime.datetime | None = None, + context: Context | None = None, + ) -> None: + """Initialize a new event.""" + self.event_type = event_type + self.data = data or {} + self.origin = origin + self.time_fired = time_fired or dt_util.utcnow() + self.context: Context = context or Context() + + def __hash__(self) -> int: + """Make hashable.""" + # The only event type that shares context are the TIME_CHANGED + return hash((self.event_type, self.context.id, self.time_fired)) + + def as_dict(self) -> dict[str, Any]: + """Create a dict representation of this Event. + + Async friendly. + """ + return { + "event_type": self.event_type, + "data": dict(self.data), + "origin": str(self.origin.value), + "time_fired": self.time_fired.isoformat(), + "context": self.context.as_dict(), + } + + def __repr__(self) -> str: + """Return the representation.""" + if self.data: + return f"" + + return f"" + + def __eq__(self, other: Any) -> bool: + """Return the comparison.""" + return ( # type: ignore + self.__class__ == other.__class__ + and self.event_type == other.event_type + and self.data == other.data + and self.origin == other.origin + and self.time_fired == other.time_fired + and self.context == other.context + ) + + +class EventBus: + """Allow the firing of and listening for events.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize a new event bus.""" + self._listeners: dict[str, list[tuple[HassJob, Callable | None]]] = {} + self._hass = hass + + @callback + def async_listeners(self) -> dict[str, int]: + """Return dictionary with events and the number of listeners. + + This method must be run in the event loop. + """ + return {key: len(self._listeners[key]) for key in self._listeners} + + @property + def listeners(self) -> dict[str, int]: + """Return dictionary with events and the number of listeners.""" + return run_callback_threadsafe(self._hass.loop, self.async_listeners).result() + + def fire( + self, + event_type: str, + event_data: dict | None = None, + origin: EventOrigin = EventOrigin.local, + context: Context | None = None, + ) -> None: + """Fire an event.""" + self._hass.loop.call_soon_threadsafe( + self.async_fire, event_type, event_data, origin, context + ) + + @callback + def async_fire( + self, + event_type: str, + event_data: dict[str, Any] | None = None, + origin: EventOrigin = EventOrigin.local, + context: Context | None = None, + time_fired: datetime.datetime | None = None, + ) -> None: + """Fire an event. + + This method must be run in the event loop. + """ + if len(event_type) > MAX_LENGTH_EVENT_TYPE: + raise MaxLengthExceeded(event_type, "event_type", MAX_LENGTH_EVENT_TYPE) + + listeners = self._listeners.get(event_type, []) + + # EVENT_HOMEASSISTANT_CLOSE should go only to his listeners + match_all_listeners = self._listeners.get(MATCH_ALL) + if match_all_listeners is not None and event_type != EVENT_HOMEASSISTANT_CLOSE: + listeners = match_all_listeners + listeners + + event = Event(event_type, event_data, origin, time_fired, context) + + if event_type != EVENT_TIME_CHANGED: + _LOGGER.debug("Bus:Handling %s", event) + + if not listeners: + return + + for job, event_filter in listeners: + if event_filter is not None: + try: + if not event_filter(event): + continue + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in event filter") + continue + self._hass.async_add_hass_job(job, event) + + def listen(self, event_type: str, listener: Callable) -> CALLBACK_TYPE: + """Listen for all events or events of a specific type. + + To listen to all events specify the constant ``MATCH_ALL`` + as event_type. + """ + async_remove_listener = run_callback_threadsafe( + self._hass.loop, self.async_listen, event_type, listener + ).result() + + def remove_listener() -> None: + """Remove the listener.""" + run_callback_threadsafe(self._hass.loop, async_remove_listener).result() + + return remove_listener + + @callback + def async_listen( + self, + event_type: str, + listener: Callable, + event_filter: Callable | None = None, + ) -> CALLBACK_TYPE: + """Listen for all events or events of a specific type. + + To listen to all events specify the constant ``MATCH_ALL`` + as event_type. + + An optional event_filter, which must be a callable decorated with + @callback that returns a boolean value, determines if the + listener callable should run. + + This method must be run in the event loop. + """ + if event_filter is not None and not is_callback(event_filter): + raise HomeAssistantError(f"Event filter {event_filter} is not a callback") + return self._async_listen_filterable_job( + event_type, (HassJob(listener), event_filter) + ) + + @callback + def _async_listen_filterable_job( + self, event_type: str, filterable_job: tuple[HassJob, Callable | None] + ) -> CALLBACK_TYPE: + self._listeners.setdefault(event_type, []).append(filterable_job) + + def remove_listener() -> None: + """Remove the listener.""" + self._async_remove_listener(event_type, filterable_job) + + return remove_listener + + def listen_once( + self, event_type: str, listener: Callable[[Event], None] + ) -> CALLBACK_TYPE: + """Listen once for event of a specific type. + + To listen to all events specify the constant ``MATCH_ALL`` + as event_type. + + Returns function to unsubscribe the listener. + """ + async_remove_listener = run_callback_threadsafe( + self._hass.loop, self.async_listen_once, event_type, listener + ).result() + + def remove_listener() -> None: + """Remove the listener.""" + run_callback_threadsafe(self._hass.loop, async_remove_listener).result() + + return remove_listener + + @callback + def async_listen_once(self, event_type: str, listener: Callable) -> CALLBACK_TYPE: + """Listen once for event of a specific type. + + To listen to all events specify the constant ``MATCH_ALL`` + as event_type. + + Returns registered listener that can be used with remove_listener. + + This method must be run in the event loop. + """ + filterable_job: tuple[HassJob, Callable | None] | None = None + + @callback + def _onetime_listener(event: Event) -> None: + """Remove listener from event bus and then fire listener.""" + nonlocal filterable_job + if hasattr(_onetime_listener, "run"): + return + # Set variable so that we will never run twice. + # Because the event bus loop might have async_fire queued multiple + # times, its possible this listener may already be lined up + # multiple times as well. + # This will make sure the second time it does nothing. + setattr(_onetime_listener, "run", True) + assert filterable_job is not None + self._async_remove_listener(event_type, filterable_job) + self._hass.async_run_job(listener, event) + + filterable_job = (HassJob(_onetime_listener), None) + + return self._async_listen_filterable_job(event_type, filterable_job) + + @callback + def _async_remove_listener( + self, event_type: str, filterable_job: tuple[HassJob, Callable | None] + ) -> None: + """Remove a listener of a specific event_type. + + This method must be run in the event loop. + """ + try: + self._listeners[event_type].remove(filterable_job) + + # delete event_type list if empty + if not self._listeners[event_type]: + self._listeners.pop(event_type) + except (KeyError, ValueError): + # KeyError is key event_type listener did not exist + # ValueError if listener did not exist within event_type + _LOGGER.exception( + "Unable to remove unknown job listener %s", filterable_job + ) + + +class State: + """Object to represent a state within the state machine. + + entity_id: the entity that is represented. + state: the state of the entity + attributes: extra information on entity and state + last_changed: last time the state was changed, not the attributes. + last_updated: last time this object was updated. + context: Context in which it was created + domain: Domain of this state. + object_id: Object id of this state. + """ + + __slots__ = [ + "entity_id", + "state", + "attributes", + "last_changed", + "last_updated", + "context", + "domain", + "object_id", + "_as_dict", + ] + + def __init__( + self, + entity_id: str, + state: str, + attributes: Mapping[str, Any] | None = None, + last_changed: datetime.datetime | None = None, + last_updated: datetime.datetime | None = None, + context: Context | None = None, + validate_entity_id: bool | None = True, + ) -> None: + """Initialize a new state.""" + state = str(state) + + if validate_entity_id and not valid_entity_id(entity_id): + raise InvalidEntityFormatError( + f"Invalid entity id encountered: {entity_id}. " + "Format should be ." + ) + + if not valid_state(state): + raise InvalidStateError( + f"Invalid state encountered for entity ID: {entity_id}. " + "State max length is 255 characters." + ) + + self.entity_id = entity_id.lower() + self.state = state + self.attributes = MappingProxyType(attributes or {}) + self.last_updated = last_updated or dt_util.utcnow() + self.last_changed = last_changed or self.last_updated + self.context = context or Context() + self.domain, self.object_id = split_entity_id(self.entity_id) + self._as_dict: dict[str, Collection[Any]] | None = None + + @property + def name(self) -> str: + """Name of this state.""" + return self.attributes.get(ATTR_FRIENDLY_NAME) or self.object_id.replace( + "_", " " + ) + + def as_dict(self) -> dict: + """Return a dict representation of the State. + + Async friendly. + + To be used for JSON serialization. + Ensures: state == State.from_dict(state.as_dict()) + """ + if not self._as_dict: + last_changed_isoformat = self.last_changed.isoformat() + if self.last_changed == self.last_updated: + last_updated_isoformat = last_changed_isoformat + else: + last_updated_isoformat = self.last_updated.isoformat() + self._as_dict = { + "entity_id": self.entity_id, + "state": self.state, + "attributes": dict(self.attributes), + "last_changed": last_changed_isoformat, + "last_updated": last_updated_isoformat, + "context": self.context.as_dict(), + } + return self._as_dict + + @classmethod + def from_dict(cls, json_dict: dict) -> Any: + """Initialize a state from a dict. + + Async friendly. + + Ensures: state == State.from_json_dict(state.to_json_dict()) + """ + if not (json_dict and "entity_id" in json_dict and "state" in json_dict): + return None + + last_changed = json_dict.get("last_changed") + + if isinstance(last_changed, str): + last_changed = dt_util.parse_datetime(last_changed) + + last_updated = json_dict.get("last_updated") + + if isinstance(last_updated, str): + last_updated = dt_util.parse_datetime(last_updated) + + context = json_dict.get("context") + if context: + context = Context(id=context.get("id"), user_id=context.get("user_id")) + + return cls( + json_dict["entity_id"], + json_dict["state"], + json_dict.get("attributes"), + last_changed, + last_updated, + context, + ) + + def __eq__(self, other: Any) -> bool: + """Return the comparison of the state.""" + return ( # type: ignore + self.__class__ == other.__class__ + and self.entity_id == other.entity_id + and self.state == other.state + and self.attributes == other.attributes + and self.context == other.context + ) + + def __repr__(self) -> str: + """Return the representation of the states.""" + attrs = f"; {util.repr_helper(self.attributes)}" if self.attributes else "" + + return ( + f"" + ) + + +class StateMachine: + """Helper class that tracks the state of different entities.""" + + def __init__(self, bus: EventBus, loop: asyncio.events.AbstractEventLoop) -> None: + """Initialize state machine.""" + self._states: dict[str, State] = {} + self._reservations: set[str] = set() + self._bus = bus + self._loop = loop + + def entity_ids(self, domain_filter: str | None = None) -> list[str]: + """List of entity ids that are being tracked.""" + future = run_callback_threadsafe( + self._loop, self.async_entity_ids, domain_filter + ) + return future.result() + + @callback + def async_entity_ids( + self, domain_filter: str | Iterable | None = None + ) -> list[str]: + """List of entity ids that are being tracked. + + This method must be run in the event loop. + """ + if domain_filter is None: + return list(self._states) + + if isinstance(domain_filter, str): + domain_filter = (domain_filter.lower(),) + + return [ + state.entity_id + for state in self._states.values() + if state.domain in domain_filter + ] + + @callback + def async_entity_ids_count( + self, domain_filter: str | Iterable | None = None + ) -> int: + """Count the entity ids that are being tracked. + + This method must be run in the event loop. + """ + if domain_filter is None: + return len(self._states) + + if isinstance(domain_filter, str): + domain_filter = (domain_filter.lower(),) + + return len( + [None for state in self._states.values() if state.domain in domain_filter] + ) + + def all(self, domain_filter: str | Iterable | None = None) -> list[State]: + """Create a list of all states.""" + return run_callback_threadsafe( + self._loop, self.async_all, domain_filter + ).result() + + @callback + def async_all(self, domain_filter: str | Iterable | None = None) -> list[State]: + """Create a list of all states matching the filter. + + This method must be run in the event loop. + """ + if domain_filter is None: + return list(self._states.values()) + + if isinstance(domain_filter, str): + domain_filter = (domain_filter.lower(),) + + return [ + state for state in self._states.values() if state.domain in domain_filter + ] + + def get(self, entity_id: str) -> State | None: + """Retrieve state of entity_id or None if not found. + + Async friendly. + """ + return self._states.get(entity_id.lower()) + + def is_state(self, entity_id: str, state: str) -> bool: + """Test if entity exists and is in specified state. + + Async friendly. + """ + state_obj = self.get(entity_id) + return state_obj is not None and state_obj.state == state + + def remove(self, entity_id: str) -> bool: + """Remove the state of an entity. + + Returns boolean to indicate if an entity was removed. + """ + return run_callback_threadsafe( + self._loop, self.async_remove, entity_id + ).result() + + @callback + def async_remove(self, entity_id: str, context: Context | None = None) -> bool: + """Remove the state of an entity. + + Returns boolean to indicate if an entity was removed. + + This method must be run in the event loop. + """ + entity_id = entity_id.lower() + old_state = self._states.pop(entity_id, None) + + if entity_id in self._reservations: + self._reservations.remove(entity_id) + + if old_state is None: + return False + + self._bus.async_fire( + EVENT_STATE_CHANGED, + {"entity_id": entity_id, "old_state": old_state, "new_state": None}, + EventOrigin.local, + context=context, + ) + return True + + def set( + self, + entity_id: str, + new_state: str, + attributes: Mapping[str, Any] | None = None, + force_update: bool = False, + context: Context | None = None, + ) -> None: + """Set the state of an entity, add entity if it does not exist. + + Attributes is an optional dict to specify attributes of this state. + + If you just update the attributes and not the state, last changed will + not be affected. + """ + run_callback_threadsafe( + self._loop, + self.async_set, + entity_id, + new_state, + attributes, + force_update, + context, + ).result() + + @callback + def async_reserve(self, entity_id: str) -> None: + """Reserve a state in the state machine for an entity being added. + + This must not fire an event when the state is reserved. + + This avoids a race condition where multiple entities with the same + entity_id are added. + """ + entity_id = entity_id.lower() + if entity_id in self._states or entity_id in self._reservations: + raise HomeAssistantError( + "async_reserve must not be called once the state is in the state machine." + ) + + self._reservations.add(entity_id) + + @callback + def async_available(self, entity_id: str) -> bool: + """Check to see if an entity_id is available to be used.""" + entity_id = entity_id.lower() + return entity_id not in self._states and entity_id not in self._reservations + + @callback + def async_set( + self, + entity_id: str, + new_state: str, + attributes: Mapping[str, Any] | None = None, + force_update: bool = False, + context: Context | None = None, + ) -> None: + """Set the state of an entity, add entity if it does not exist. + + Attributes is an optional dict to specify attributes of this state. + + If you just update the attributes and not the state, last changed will + not be affected. + + This method must be run in the event loop. + """ + entity_id = entity_id.lower() + new_state = str(new_state) + attributes = attributes or {} + old_state = self._states.get(entity_id) + if old_state is None: + same_state = False + same_attr = False + last_changed = None + else: + same_state = old_state.state == new_state and not force_update + same_attr = old_state.attributes == MappingProxyType(attributes) + last_changed = old_state.last_changed if same_state else None + + if same_state and same_attr: + return + + if context is None: + context = Context() + + now = dt_util.utcnow() + + state = State( + entity_id, + new_state, + attributes, + last_changed, + now, + context, + old_state is None, + ) + self._states[entity_id] = state + self._bus.async_fire( + EVENT_STATE_CHANGED, + {"entity_id": entity_id, "old_state": old_state, "new_state": state}, + EventOrigin.local, + context, + time_fired=now, + ) + + +class Service: + """Representation of a callable service.""" + + __slots__ = ["job", "schema"] + + def __init__( + self, + func: Callable, + schema: vol.Schema | None, + context: Context | None = None, + ) -> None: + """Initialize a service.""" + self.job = HassJob(func) + self.schema = schema + + +class ServiceCall: + """Representation of a call to a service.""" + + __slots__ = ["domain", "service", "data", "context"] + + def __init__( + self, + domain: str, + service: str, + data: dict | None = None, + context: Context | None = None, + ) -> None: + """Initialize a service call.""" + self.domain = domain.lower() + self.service = service.lower() + self.data = MappingProxyType(data or {}) + self.context = context or Context() + + def __repr__(self) -> str: + """Return the representation of the service.""" + if self.data: + return ( + f"" + ) + + return f"" + + +class ServiceRegistry: + """Offer the services over the eventbus.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize a service registry.""" + self._services: dict[str, dict[str, Service]] = {} + self._hass = hass + + @property + def services(self) -> dict[str, dict[str, Service]]: + """Return dictionary with per domain a list of available services.""" + return run_callback_threadsafe(self._hass.loop, self.async_services).result() + + @callback + def async_services(self) -> dict[str, dict[str, Service]]: + """Return dictionary with per domain a list of available services. + + This method must be run in the event loop. + """ + return {domain: self._services[domain].copy() for domain in self._services} + + def has_service(self, domain: str, service: str) -> bool: + """Test if specified service exists. + + Async friendly. + """ + return service.lower() in self._services.get(domain.lower(), []) + + def register( + self, + domain: str, + service: str, + service_func: Callable, + schema: vol.Schema | None = None, + ) -> None: + """ + Register a service. + + Schema is called to coerce and validate the service data. + """ + run_callback_threadsafe( + self._hass.loop, self.async_register, domain, service, service_func, schema + ).result() + + @callback + def async_register( + self, + domain: str, + service: str, + service_func: Callable, + schema: vol.Schema | None = None, + ) -> None: + """ + Register a service. + + Schema is called to coerce and validate the service data. + + This method must be run in the event loop. + """ + domain = domain.lower() + service = service.lower() + service_obj = Service(service_func, schema) + + if domain in self._services: + self._services[domain][service] = service_obj + else: + self._services[domain] = {service: service_obj} + + self._hass.bus.async_fire( + EVENT_SERVICE_REGISTERED, {ATTR_DOMAIN: domain, ATTR_SERVICE: service} + ) + + def remove(self, domain: str, service: str) -> None: + """Remove a registered service from service handler.""" + run_callback_threadsafe( + self._hass.loop, self.async_remove, domain, service + ).result() + + @callback + def async_remove(self, domain: str, service: str) -> None: + """Remove a registered service from service handler. + + This method must be run in the event loop. + """ + domain = domain.lower() + service = service.lower() + + if service not in self._services.get(domain, {}): + _LOGGER.warning("Unable to remove unknown service %s/%s", domain, service) + return + + self._services[domain].pop(service) + + if not self._services[domain]: + self._services.pop(domain) + + self._hass.bus.async_fire( + EVENT_SERVICE_REMOVED, {ATTR_DOMAIN: domain, ATTR_SERVICE: service} + ) + + def call( + self, + domain: str, + service: str, + service_data: dict | None = None, + blocking: bool = False, + context: Context | None = None, + limit: float | None = SERVICE_CALL_LIMIT, + target: dict | None = None, + ) -> bool | None: + """ + Call a service. + + See description of async_call for details. + """ + return asyncio.run_coroutine_threadsafe( + self.async_call( + domain, service, service_data, blocking, context, limit, target + ), + self._hass.loop, + ).result() + + async def async_call( + self, + domain: str, + service: str, + service_data: dict | None = None, + blocking: bool = False, + context: Context | None = None, + limit: float | None = SERVICE_CALL_LIMIT, + target: dict | None = None, + ) -> bool | None: + """ + Call a service. + + Specify blocking=True to wait until service is executed. + Waits a maximum of limit, which may be None for no timeout. + + If blocking = True, will return boolean if service executed + successfully within limit. + + This method will fire an event to indicate the service has been called. + + Because the service is sent as an event you are not allowed to use + the keys ATTR_DOMAIN and ATTR_SERVICE in your service_data. + + This method is a coroutine. + """ + domain = domain.lower() + service = service.lower() + context = context or Context() + service_data = service_data or {} + + try: + handler = self._services[domain][service] + except KeyError: + raise ServiceNotFound(domain, service) from None + + if target: + service_data.update(target) + + if handler.schema: + try: + processed_data = handler.schema(service_data) + except vol.Invalid: + _LOGGER.debug( + "Invalid data for service call %s.%s: %s", + domain, + service, + service_data, + ) + raise + else: + processed_data = service_data + + service_call = ServiceCall(domain, service, processed_data, context) + + self._hass.bus.async_fire( + EVENT_CALL_SERVICE, + { + ATTR_DOMAIN: domain.lower(), + ATTR_SERVICE: service.lower(), + ATTR_SERVICE_DATA: service_data, + }, + context=context, + ) + + coro = self._execute_service(handler, service_call) + if not blocking: + self._run_service_in_background(coro, service_call) + return None + + task = self._hass.async_create_task(coro) + try: + await asyncio.wait({task}, timeout=limit) + except asyncio.CancelledError: + # Task calling us was cancelled, so cancel service call task, and wait for + # it to be cancelled, within reason, before leaving. + _LOGGER.debug("Service call was cancelled: %s", service_call) + task.cancel() + await asyncio.wait({task}, timeout=SERVICE_CALL_LIMIT) + raise + + if task.cancelled(): + # Service call task was cancelled some other way, such as during shutdown. + _LOGGER.debug("Service was cancelled: %s", service_call) + raise asyncio.CancelledError + if task.done(): + # Propagate any exceptions that might have happened during service call. + task.result() + # Service call completed successfully! + return True + # Service call task did not complete before timeout expired. + # Let it keep running in background. + self._run_service_in_background(task, service_call) + _LOGGER.debug("Service did not complete before timeout: %s", service_call) + return False + + def _run_service_in_background( + self, coro_or_task: Coroutine | asyncio.Task, service_call: ServiceCall + ) -> None: + """Run service call in background, catching and logging any exceptions.""" + + async def catch_exceptions() -> None: + try: + await coro_or_task + except Unauthorized: + _LOGGER.warning( + "Unauthorized service called %s/%s", + service_call.domain, + service_call.service, + ) + except asyncio.CancelledError: + _LOGGER.debug("Service was cancelled: %s", service_call) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error executing service: %s", service_call) + + self._hass.async_create_task(catch_exceptions()) + + async def _execute_service( + self, handler: Service, service_call: ServiceCall + ) -> None: + """Execute a service.""" + if handler.job.job_type == HassJobType.Coroutinefunction: + await handler.job.target(service_call) + elif handler.job.job_type == HassJobType.Callback: + handler.job.target(service_call) + else: + await self._hass.async_add_executor_job(handler.job.target, service_call) + + +class Config: + """Configuration settings for Home Assistant.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize a new config object.""" + self.hass = hass + + self.latitude: float = 0 + self.longitude: float = 0 + self.elevation: int = 0 + self.location_name: str = "Home" + self.time_zone: str = "UTC" + self.units: UnitSystem = METRIC_SYSTEM + self.internal_url: str | None = None + self.external_url: str | None = None + + self.config_source: str = "default" + + # If True, pip install is skipped for requirements on startup + self.skip_pip: bool = False + + # List of loaded components + self.components: set[str] = set() + + # API (HTTP) server configuration, see components.http.ApiConfig + self.api: Any | None = None + + # Directory that holds the configuration + self.config_dir: str | None = None + + # List of allowed external dirs to access + self.allowlist_external_dirs: set[str] = set() + + # List of allowed external URLs that integrations may use + self.allowlist_external_urls: set[str] = set() + + # Dictionary of Media folders that integrations may use + self.media_dirs: dict[str, str] = {} + + # If Home Assistant is running in safe mode + self.safe_mode: bool = False + + # Use legacy template behavior + self.legacy_templates: bool = False + + def distance(self, lat: float, lon: float) -> float | None: + """Calculate distance from Home Assistant. + + Async friendly. + """ + return self.units.length( + location.distance(self.latitude, self.longitude, lat, lon), LENGTH_METERS + ) + + def path(self, *path: str) -> str: + """Generate path to the file within the configuration directory. + + Async friendly. + """ + if self.config_dir is None: + raise HomeAssistantError("config_dir is not set") + return os.path.join(self.config_dir, *path) + + def is_allowed_external_url(self, url: str) -> bool: + """Check if an external URL is allowed.""" + parsed_url = f"{str(yarl.URL(url))}/" + + return any( + allowed + for allowed in self.allowlist_external_urls + if parsed_url.startswith(allowed) + ) + + def is_allowed_path(self, path: str) -> bool: + """Check if the path is valid for access from outside.""" + assert path is not None + + thepath = pathlib.Path(path) + try: + # The file path does not have to exist (it's parent should) + if thepath.exists(): + thepath = thepath.resolve() + else: + thepath = thepath.parent.resolve() + except (FileNotFoundError, RuntimeError, PermissionError): + return False + + for allowed_path in self.allowlist_external_dirs: + try: + thepath.relative_to(allowed_path) + return True + except ValueError: + pass + + return False + + def as_dict(self) -> dict: + """Create a dictionary representation of the configuration. + + Async friendly. + """ + return { + "latitude": self.latitude, + "longitude": self.longitude, + "elevation": self.elevation, + "unit_system": self.units.as_dict(), + "location_name": self.location_name, + "time_zone": self.time_zone, + "components": self.components, + "config_dir": self.config_dir, + # legacy, backwards compat + "whitelist_external_dirs": self.allowlist_external_dirs, + "allowlist_external_dirs": self.allowlist_external_dirs, + "allowlist_external_urls": self.allowlist_external_urls, + "version": __version__, + "config_source": self.config_source, + "safe_mode": self.safe_mode, + "state": self.hass.state.value, + "external_url": self.external_url, + "internal_url": self.internal_url, + } + + def set_time_zone(self, time_zone_str: str) -> None: + """Help to set the time zone.""" + time_zone = dt_util.get_time_zone(time_zone_str) + + if time_zone: + self.time_zone = time_zone_str + dt_util.set_default_time_zone(time_zone) + else: + raise ValueError(f"Received invalid time zone {time_zone_str}") + + @callback + def _update( + self, + *, + source: str, + latitude: float | None = None, + longitude: float | None = None, + elevation: int | None = None, + unit_system: str | None = None, + location_name: str | None = None, + time_zone: str | None = None, + # pylint: disable=dangerous-default-value # _UNDEFs not modified + external_url: str | dict | None = _UNDEF, + internal_url: str | dict | None = _UNDEF, + ) -> None: + """Update the configuration from a dictionary.""" + self.config_source = source + if latitude is not None: + self.latitude = latitude + if longitude is not None: + self.longitude = longitude + if elevation is not None: + self.elevation = elevation + if unit_system is not None: + if unit_system == CONF_UNIT_SYSTEM_IMPERIAL: + self.units = IMPERIAL_SYSTEM + else: + self.units = METRIC_SYSTEM + if location_name is not None: + self.location_name = location_name + if time_zone is not None: + self.set_time_zone(time_zone) + if external_url is not _UNDEF: + self.external_url = cast(Optional[str], external_url) + if internal_url is not _UNDEF: + self.internal_url = cast(Optional[str], internal_url) + + async def async_update(self, **kwargs: Any) -> None: + """Update the configuration from a dictionary.""" + self._update(source=SOURCE_STORAGE, **kwargs) + await self.async_store() + self.hass.bus.async_fire(EVENT_CORE_CONFIG_UPDATE, kwargs) + + async def async_load(self) -> None: + """Load [homeassistant] core config.""" + store = self.hass.helpers.storage.Store( + CORE_STORAGE_VERSION, CORE_STORAGE_KEY, private=True + ) + 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), + ) + + async def async_store(self) -> None: + """Store [homeassistant] core config.""" + data = { + "latitude": self.latitude, + "longitude": self.longitude, + "elevation": self.elevation, + "unit_system": self.units.name, + "location_name": self.location_name, + "time_zone": self.time_zone, + "external_url": self.external_url, + "internal_url": self.internal_url, + } + + store = self.hass.helpers.storage.Store( + CORE_STORAGE_VERSION, CORE_STORAGE_KEY, private=True + ) + await store.async_save(data) + + +def _async_create_timer(hass: HomeAssistant) -> None: + """Create a timer that will start on HOMEASSISTANT_START.""" + handle = None + timer_context = Context() + + def schedule_tick(now: datetime.datetime) -> None: + """Schedule a timer tick when the next second rolls around.""" + nonlocal handle + + slp_seconds = 1 - (now.microsecond / 10 ** 6) + target = monotonic() + slp_seconds + handle = hass.loop.call_later(slp_seconds, fire_time_event, target) + + @callback + def fire_time_event(target: float) -> None: + """Fire next time event.""" + now = dt_util.utcnow() + + hass.bus.async_fire( + EVENT_TIME_CHANGED, {ATTR_NOW: now}, time_fired=now, context=timer_context + ) + + # If we are more than a second late, a tick was missed + late = monotonic() - target + if late > 1: + hass.bus.async_fire( + EVENT_TIMER_OUT_OF_SYNC, + {ATTR_SECONDS: late}, + time_fired=now, + context=timer_context, + ) + + schedule_tick(now) + + @callback + def stop_timer(_: Event) -> None: + """Stop the timer.""" + if handle is not None: + handle.cancel() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_timer) + + _LOGGER.info("Timer:starting") + schedule_tick(dt_util.utcnow()) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/data_entry_flow.py b/homeassistant-2021.6.0.dev0/homeassistant/data_entry_flow.py new file mode 100644 index 00000000000..dd8b1c53a68 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/data_entry_flow.py @@ -0,0 +1,461 @@ +"""Classes to help gather user submissions.""" +from __future__ import annotations + +import abc +import asyncio +from collections.abc import Mapping +from types import MappingProxyType +from typing import Any, TypedDict +import uuid + +import voluptuous as vol + +from .core import HomeAssistant, callback +from .exceptions import HomeAssistantError + +RESULT_TYPE_FORM = "form" +RESULT_TYPE_CREATE_ENTRY = "create_entry" +RESULT_TYPE_ABORT = "abort" +RESULT_TYPE_EXTERNAL_STEP = "external" +RESULT_TYPE_EXTERNAL_STEP_DONE = "external_done" +RESULT_TYPE_SHOW_PROGRESS = "progress" +RESULT_TYPE_SHOW_PROGRESS_DONE = "progress_done" + +# Event that is fired when a flow is progressed via external or progress source. +EVENT_DATA_ENTRY_FLOW_PROGRESSED = "data_entry_flow_progressed" + + +class FlowError(HomeAssistantError): + """Error while configuring an account.""" + + +class UnknownHandler(FlowError): + """Unknown handler specified.""" + + +class UnknownFlow(FlowError): + """Unknown flow specified.""" + + +class UnknownStep(FlowError): + """Unknown step specified.""" + + +class AbortFlow(FlowError): + """Exception to indicate a flow needs to be aborted.""" + + def __init__(self, reason: str, description_placeholders: dict | None = None): + """Initialize an abort flow exception.""" + super().__init__(f"Flow aborted: {reason}") + self.reason = reason + self.description_placeholders = description_placeholders + + +class FlowResult(TypedDict, total=False): + """Typed result dict.""" + + version: int + type: str + flow_id: str + handler: str + title: str + data: Mapping[str, Any] + step_id: str + data_schema: vol.Schema + extra: str + required: bool + errors: dict[str, str] | None + description: str | None + description_placeholders: dict[str, Any] | None + progress_action: str + url: str + reason: str + context: dict[str, Any] + result: Any + last_step: bool | None + options: Mapping[str, Any] + + +class FlowManager(abc.ABC): + """Manage all the flows that are in progress.""" + + def __init__( + self, + hass: HomeAssistant, + ) -> None: + """Initialize the flow manager.""" + self.hass = hass + self._initializing: dict[str, list[asyncio.Future]] = {} + self._initialize_tasks: dict[str, list[asyncio.Task]] = {} + self._progress: dict[str, Any] = {} + + async def async_wait_init_flow_finish(self, handler: str) -> None: + """Wait till all flows in progress are initialized.""" + current = self._initializing.get(handler) + + if not current: + return + + await asyncio.wait(current) + + @abc.abstractmethod + async def async_create_flow( + self, + handler_key: Any, + *, + context: dict[str, Any] | None = None, + data: dict[str, Any] | None = None, + ) -> FlowHandler: + """Create a flow for specified handler. + + Handler key is the domain of the component that we want to set up. + """ + + @abc.abstractmethod + async def async_finish_flow( + self, flow: FlowHandler, result: FlowResult + ) -> FlowResult: + """Finish a config flow and add an entry.""" + + async def async_post_init(self, flow: FlowHandler, result: FlowResult) -> None: + """Entry has finished executing its first step asynchronously.""" + + @callback + def async_progress(self, include_uninitialized: bool = False) -> list[FlowResult]: + """Return the flows in progress.""" + return [ + { + "flow_id": flow.flow_id, + "handler": flow.handler, + "context": flow.context, + "step_id": flow.cur_step["step_id"] if flow.cur_step else None, + } + for flow in self._progress.values() + if include_uninitialized or flow.cur_step is not None + ] + + async def async_init( + self, handler: str, *, context: dict[str, Any] | None = None, data: Any = None + ) -> FlowResult: + """Start a configuration flow.""" + if context is None: + context = {} + + init_done: asyncio.Future = asyncio.Future() + self._initializing.setdefault(handler, []).append(init_done) + + task = asyncio.create_task(self._async_init(init_done, handler, context, data)) + self._initialize_tasks.setdefault(handler, []).append(task) + + try: + flow, result = await task + finally: + self._initialize_tasks[handler].remove(task) + self._initializing[handler].remove(init_done) + + if result["type"] != RESULT_TYPE_ABORT: + await self.async_post_init(flow, result) + + return result + + async def _async_init( + self, + init_done: asyncio.Future, + handler: str, + context: dict, + data: Any, + ) -> tuple[FlowHandler, FlowResult]: + """Run the init in a task to allow it to be canceled at shutdown.""" + flow = await self.async_create_flow(handler, context=context, data=data) + if not flow: + raise UnknownFlow("Flow was not created") + flow.hass = self.hass + flow.handler = handler + flow.flow_id = uuid.uuid4().hex + flow.context = context + self._progress[flow.flow_id] = flow + result = await self._async_handle_step(flow, flow.init_step, data, init_done) + return flow, result + + async def async_shutdown(self) -> None: + """Cancel any initializing flows.""" + for task_list in self._initialize_tasks.values(): + for task in task_list: + task.cancel() + + async def async_configure( + self, flow_id: str, user_input: dict | None = None + ) -> FlowResult: + """Continue a configuration flow.""" + flow = self._progress.get(flow_id) + + if flow is None: + raise UnknownFlow + + cur_step = flow.cur_step + + if cur_step.get("data_schema") is not None and user_input is not None: + user_input = cur_step["data_schema"](user_input) + + result = await self._async_handle_step(flow, cur_step["step_id"], user_input) + + if cur_step["type"] in (RESULT_TYPE_EXTERNAL_STEP, RESULT_TYPE_SHOW_PROGRESS): + if cur_step["type"] == RESULT_TYPE_EXTERNAL_STEP and result["type"] not in ( + RESULT_TYPE_EXTERNAL_STEP, + RESULT_TYPE_EXTERNAL_STEP_DONE, + ): + raise ValueError( + "External step can only transition to " + "external step or external step done." + ) + if cur_step["type"] == RESULT_TYPE_SHOW_PROGRESS and result["type"] not in ( + RESULT_TYPE_SHOW_PROGRESS, + RESULT_TYPE_SHOW_PROGRESS_DONE, + ): + raise ValueError( + "Show progress can only transition to show progress or show progress done." + ) + + # If the result has changed from last result, fire event to update + # the frontend. + if ( + cur_step["step_id"] != result.get("step_id") + or result["type"] == RESULT_TYPE_SHOW_PROGRESS + ): + # Tell frontend to reload the flow state. + self.hass.bus.async_fire( + EVENT_DATA_ENTRY_FLOW_PROGRESSED, + {"handler": flow.handler, "flow_id": flow_id, "refresh": True}, + ) + + return result + + @callback + def async_abort(self, flow_id: str) -> None: + """Abort a flow.""" + if self._progress.pop(flow_id, None) is None: + raise UnknownFlow + + async def _async_handle_step( + self, + flow: Any, + step_id: str, + user_input: dict | None, + step_done: asyncio.Future | None = None, + ) -> FlowResult: + """Handle a step of a flow.""" + method = f"async_step_{step_id}" + + if not hasattr(flow, method): + self._progress.pop(flow.flow_id) + if step_done: + step_done.set_result(None) + raise UnknownStep( + f"Handler {flow.__class__.__name__} doesn't support step {step_id}" + ) + + try: + result: FlowResult = await getattr(flow, method)(user_input) + except AbortFlow as err: + result = _create_abort_data( + flow.flow_id, flow.handler, err.reason, err.description_placeholders + ) + + # Mark the step as done. + # We do this before calling async_finish_flow because config entries will hit a + # circular dependency where async_finish_flow sets up new entry, which needs the + # integration to be set up, which is waiting for init to be done. + if step_done: + step_done.set_result(None) + + if result["type"] not in ( + RESULT_TYPE_FORM, + RESULT_TYPE_EXTERNAL_STEP, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_ABORT, + RESULT_TYPE_EXTERNAL_STEP_DONE, + RESULT_TYPE_SHOW_PROGRESS, + RESULT_TYPE_SHOW_PROGRESS_DONE, + ): + raise ValueError(f"Handler returned incorrect type: {result['type']}") + + if result["type"] in ( + RESULT_TYPE_FORM, + RESULT_TYPE_EXTERNAL_STEP, + RESULT_TYPE_EXTERNAL_STEP_DONE, + RESULT_TYPE_SHOW_PROGRESS, + RESULT_TYPE_SHOW_PROGRESS_DONE, + ): + flow.cur_step = result + return result + + # We pass a copy of the result because we're mutating our version + result = await self.async_finish_flow(flow, result.copy()) + + # _async_finish_flow may change result type, check it again + if result["type"] == RESULT_TYPE_FORM: + flow.cur_step = result + return result + + # Abort and Success results both finish the flow + self._progress.pop(flow.flow_id) + + return result + + +class FlowHandler: + """Handle the configuration flow of a component.""" + + # Set by flow manager + cur_step: dict[str, str] | None = None + + # While not purely typed, it makes typehinting more useful for us + # and removes the need for constant None checks or asserts. + flow_id: str = None # type: ignore + hass: HomeAssistant = None # type: ignore + handler: str = None # type: ignore + # Ensure the attribute has a subscriptable, but immutable, default value. + context: dict[str, Any] = MappingProxyType({}) # type: ignore + + # Set by _async_create_flow callback + init_step = "init" + + # Set by developer + VERSION = 1 + + @property + def source(self) -> str | None: + """Source that initialized the flow.""" + if not hasattr(self, "context"): + return None + + return self.context.get("source", None) + + @property + def show_advanced_options(self) -> bool: + """If we should show advanced options.""" + if not hasattr(self, "context"): + return False + + return self.context.get("show_advanced_options", False) + + @callback + def async_show_form( + self, + *, + step_id: str, + data_schema: vol.Schema = None, + errors: dict[str, str] | None = None, + description_placeholders: dict[str, Any] | None = None, + last_step: bool | None = None, + ) -> FlowResult: + """Return the definition of a form to gather user input.""" + return { + "type": RESULT_TYPE_FORM, + "flow_id": self.flow_id, + "handler": self.handler, + "step_id": step_id, + "data_schema": data_schema, + "errors": errors, + "description_placeholders": description_placeholders, + "last_step": last_step, # Display next or submit button in frontend + } + + @callback + def async_create_entry( + self, + *, + title: str, + data: Mapping[str, Any], + description: str | None = None, + description_placeholders: dict | None = None, + ) -> FlowResult: + """Finish config flow and create a config entry.""" + return { + "version": self.VERSION, + "type": RESULT_TYPE_CREATE_ENTRY, + "flow_id": self.flow_id, + "handler": self.handler, + "title": title, + "data": data, + "description": description, + "description_placeholders": description_placeholders, + } + + @callback + def async_abort( + self, *, reason: str, description_placeholders: dict | None = None + ) -> FlowResult: + """Abort the config flow.""" + return _create_abort_data( + self.flow_id, self.handler, reason, description_placeholders + ) + + @callback + def async_external_step( + self, *, step_id: str, url: str, description_placeholders: dict | None = None + ) -> FlowResult: + """Return the definition of an external step for the user to take.""" + return { + "type": RESULT_TYPE_EXTERNAL_STEP, + "flow_id": self.flow_id, + "handler": self.handler, + "step_id": step_id, + "url": url, + "description_placeholders": description_placeholders, + } + + @callback + def async_external_step_done(self, *, next_step_id: str) -> FlowResult: + """Return the definition of an external step for the user to take.""" + return { + "type": RESULT_TYPE_EXTERNAL_STEP_DONE, + "flow_id": self.flow_id, + "handler": self.handler, + "step_id": next_step_id, + } + + @callback + def async_show_progress( + self, + *, + step_id: str, + progress_action: str, + description_placeholders: dict | None = None, + ) -> FlowResult: + """Show a progress message to the user, without user input allowed.""" + return { + "type": RESULT_TYPE_SHOW_PROGRESS, + "flow_id": self.flow_id, + "handler": self.handler, + "step_id": step_id, + "progress_action": progress_action, + "description_placeholders": description_placeholders, + } + + @callback + def async_show_progress_done(self, *, next_step_id: str) -> FlowResult: + """Mark the progress done.""" + return { + "type": RESULT_TYPE_SHOW_PROGRESS_DONE, + "flow_id": self.flow_id, + "handler": self.handler, + "step_id": next_step_id, + } + + +@callback +def _create_abort_data( + flow_id: str, + handler: str, + reason: str, + description_placeholders: dict | None = None, +) -> FlowResult: + """Return the definition of an external step for the user to take.""" + return { + "type": RESULT_TYPE_ABORT, + "flow_id": flow_id, + "handler": handler, + "reason": reason, + "description_placeholders": description_placeholders, + } diff --git a/homeassistant-2021.6.0.dev0/homeassistant/exceptions.py b/homeassistant-2021.6.0.dev0/homeassistant/exceptions.py new file mode 100644 index 00000000000..844fd369cac --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/exceptions.py @@ -0,0 +1,201 @@ +"""The exceptions used by Home Assistant.""" +from __future__ import annotations + +from collections.abc import Generator, Sequence +from typing import TYPE_CHECKING + +import attr + +if TYPE_CHECKING: + from .core import Context + + +class HomeAssistantError(Exception): + """General Home Assistant exception occurred.""" + + +class InvalidEntityFormatError(HomeAssistantError): + """When an invalid formatted entity is encountered.""" + + +class NoEntitySpecifiedError(HomeAssistantError): + """When no entity is specified.""" + + +class TemplateError(HomeAssistantError): + """Error during template rendering.""" + + def __init__(self, exception: Exception) -> None: + """Init the error.""" + super().__init__(f"{exception.__class__.__name__}: {exception}") + + +@attr.s +class ConditionError(HomeAssistantError): + """Error during condition evaluation.""" + + # The type of the failed condition, such as 'and' or 'numeric_state' + type: str = attr.ib() + + @staticmethod + def _indent(indent: int, message: str) -> str: + """Return indentation.""" + return " " * indent + message + + def output(self, indent: int) -> Generator: + """Yield an indented representation.""" + raise NotImplementedError() + + def __str__(self) -> str: + """Return string representation.""" + return "\n".join(list(self.output(indent=0))) + + +@attr.s +class ConditionErrorMessage(ConditionError): + """Condition error message.""" + + # A message describing this error + message: str = attr.ib() + + def output(self, indent: int) -> Generator: + """Yield an indented representation.""" + yield self._indent(indent, f"In '{self.type}' condition: {self.message}") + + +@attr.s +class ConditionErrorIndex(ConditionError): + """Condition error with index.""" + + # The zero-based index of the failed condition, for conditions with multiple parts + index: int = attr.ib() + # The total number of parts in this condition, including non-failed parts + total: int = attr.ib() + # The error that this error wraps + error: ConditionError = attr.ib() + + def output(self, indent: int) -> Generator: + """Yield an indented representation.""" + if self.total > 1: + yield self._indent( + indent, f"In '{self.type}' (item {self.index+1} of {self.total}):" + ) + else: + yield self._indent(indent, f"In '{self.type}':") + + yield from self.error.output(indent + 1) + + +@attr.s +class ConditionErrorContainer(ConditionError): + """Condition error with subconditions.""" + + # List of ConditionErrors that this error wraps + errors: Sequence[ConditionError] = attr.ib() + + def output(self, indent: int) -> Generator: + """Yield an indented representation.""" + for item in self.errors: + yield from item.output(indent) + + +class IntegrationError(HomeAssistantError): + """Base class for platform and config entry exceptions.""" + + def __str__(self) -> str: + """Return a human readable error.""" + return super().__str__() or str(self.__cause__) + + +class PlatformNotReady(IntegrationError): + """Error to indicate that platform is not ready.""" + + +class ConfigEntryNotReady(IntegrationError): + """Error to indicate that config entry is not ready.""" + + +class ConfigEntryAuthFailed(IntegrationError): + """Error to indicate that config entry could not authenticate.""" + + +class InvalidStateError(HomeAssistantError): + """When an invalid state is encountered.""" + + +class Unauthorized(HomeAssistantError): + """When an action is unauthorized.""" + + def __init__( + self, + context: Context | None = None, + user_id: str | None = None, + entity_id: str | None = None, + config_entry_id: str | None = None, + perm_category: str | None = None, + permission: str | None = None, + ) -> None: + """Unauthorized error.""" + super().__init__(self.__class__.__name__) + self.context = context + + if user_id is None and context is not None: + user_id = context.user_id + + self.user_id = user_id + self.entity_id = entity_id + self.config_entry_id = config_entry_id + # Not all actions have an ID (like adding config entry) + # We then use this fallback to know what category was unauth + self.perm_category = perm_category + self.permission = permission + + +class UnknownUser(Unauthorized): + """When call is made with user ID that doesn't exist.""" + + +class ServiceNotFound(HomeAssistantError): + """Raised when a service is not found.""" + + def __init__(self, domain: str, service: str) -> None: + """Initialize error.""" + super().__init__(self, f"Service {domain}.{service} not found") + self.domain = domain + self.service = service + + def __str__(self) -> str: + """Return string representation.""" + return f"Unable to find service {self.domain}.{self.service}" + + +class MaxLengthExceeded(HomeAssistantError): + """Raised when a property value has exceeded the max character length.""" + + def __init__(self, value: str, property_name: str, max_length: int) -> None: + """Initialize error.""" + super().__init__( + self, + ( + f"Value {value} for property {property_name} has a max length of " + f"{max_length} characters" + ), + ) + self.value = value + self.property_name = property_name + self.max_length = max_length + + +class RequiredParameterMissing(HomeAssistantError): + """Raised when a required parameter is missing from a function call.""" + + def __init__(self, parameter_names: list[str]) -> None: + """Initialize error.""" + super().__init__( + self, + ( + "Call must include at least one of the following parameters: " + f"{', '.join(parameter_names)}" + ), + ) + self.parameter_names = parameter_names diff --git a/homeassistant-2021.6.0.dev0/homeassistant/loader.py b/homeassistant-2021.6.0.dev0/homeassistant/loader.py new file mode 100644 index 00000000000..adebe535f6a --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/loader.py @@ -0,0 +1,774 @@ +""" +The methods for loading Home Assistant integrations. + +This module has quite some complex parts. I have tried to add as much +documentation as possible to keep it understandable. +""" +from __future__ import annotations + +import asyncio +from contextlib import suppress +import functools as ft +import importlib +import json +import logging +import pathlib +import sys +from types import ModuleType +from typing import TYPE_CHECKING, Any, Callable, Dict, TypedDict, TypeVar, cast + +from awesomeversion import ( + AwesomeVersion, + AwesomeVersionException, + AwesomeVersionStrategy, +) + +from homeassistant.generated.dhcp import DHCP +from homeassistant.generated.mqtt import MQTT +from homeassistant.generated.ssdp import SSDP +from homeassistant.generated.zeroconf import HOMEKIT, ZEROCONF +from homeassistant.util.async_ import gather_with_concurrency + +# Typing imports that create a circular dependency +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + +# mypy: disallow-any-generics + +CALLABLE_T = TypeVar( # pylint: disable=invalid-name + "CALLABLE_T", bound=Callable[..., Any] +) + +_LOGGER = logging.getLogger(__name__) + +DATA_COMPONENTS = "components" +DATA_INTEGRATIONS = "integrations" +DATA_CUSTOM_COMPONENTS = "custom_components" +PACKAGE_CUSTOM_COMPONENTS = "custom_components" +PACKAGE_BUILTIN = "homeassistant.components" +CUSTOM_WARNING = ( + "You are using a custom integration %s which has not " + "been tested by Home Assistant. This component might " + "cause stability problems, be sure to disable it if you " + "experience issues with Home Assistant" +) + +_UNDEF = object() # Internal; not helpers.typing.UNDEFINED due to circular dependency + +MAX_LOAD_CONCURRENTLY = 4 + + +class Manifest(TypedDict, total=False): + """ + Integration manifest. + + Note that none of the attributes are marked Optional here. However, some of them may be optional in manifest.json + in the sense that they can be omitted altogether. But when present, they should not have null values in it. + """ + + name: str + disabled: str + domain: str + dependencies: list[str] + after_dependencies: list[str] + requirements: list[str] + config_flow: bool + documentation: str + issue_tracker: str + quality_scale: str + iot_class: str + mqtt: list[str] + ssdp: list[dict[str, str]] + zeroconf: list[str | dict[str, str]] + dhcp: list[dict[str, str]] + homekit: dict[str, list[str]] + is_built_in: bool + version: str + codeowners: list[str] + + +def manifest_from_legacy_module(domain: str, module: ModuleType) -> Manifest: + """Generate a manifest from a legacy module.""" + return { + "domain": domain, + "name": domain, + "requirements": getattr(module, "REQUIREMENTS", []), + "dependencies": getattr(module, "DEPENDENCIES", []), + "codeowners": [], + } + + +async def _async_get_custom_components( + hass: HomeAssistant, +) -> dict[str, Integration]: + """Return list of custom integrations.""" + if hass.config.safe_mode: + return {} + + try: + import custom_components # pylint: disable=import-outside-toplevel + except ImportError: + return {} + + def get_sub_directories(paths: list[str]) -> list[pathlib.Path]: + """Return all sub directories in a set of paths.""" + return [ + entry + for path in paths + for entry in pathlib.Path(path).iterdir() + if entry.is_dir() + ] + + dirs = await hass.async_add_executor_job( + get_sub_directories, custom_components.__path__ + ) + + integrations = await gather_with_concurrency( + MAX_LOAD_CONCURRENTLY, + *( + hass.async_add_executor_job( + Integration.resolve_from_root, hass, custom_components, comp.name + ) + for comp in dirs + ), + ) + + return { + integration.domain: integration + for integration in integrations + if integration is not None + } + + +async def async_get_custom_components( + hass: HomeAssistant, +) -> dict[str, Integration]: + """Return cached list of custom integrations.""" + reg_or_evt = hass.data.get(DATA_CUSTOM_COMPONENTS) + + if reg_or_evt is None: + evt = hass.data[DATA_CUSTOM_COMPONENTS] = asyncio.Event() + + reg = await _async_get_custom_components(hass) + + hass.data[DATA_CUSTOM_COMPONENTS] = reg + evt.set() + return reg + + if isinstance(reg_or_evt, asyncio.Event): + await reg_or_evt.wait() + return cast(Dict[str, "Integration"], hass.data.get(DATA_CUSTOM_COMPONENTS)) + + return cast(Dict[str, "Integration"], reg_or_evt) + + +async def async_get_config_flows(hass: HomeAssistant) -> set[str]: + """Return cached list of config flows.""" + # pylint: disable=import-outside-toplevel + from homeassistant.generated.config_flows import FLOWS + + flows: set[str] = set() + flows.update(FLOWS) + + integrations = await async_get_custom_components(hass) + flows.update( + [ + integration.domain + for integration in integrations.values() + if integration.config_flow + ] + ) + + return flows + + +async def async_get_zeroconf(hass: HomeAssistant) -> dict[str, list[dict[str, str]]]: + """Return cached list of zeroconf types.""" + zeroconf: dict[str, list[dict[str, str]]] = ZEROCONF.copy() + + integrations = await async_get_custom_components(hass) + for integration in integrations.values(): + if not integration.zeroconf: + continue + for entry in integration.zeroconf: + data = {"domain": integration.domain} + if isinstance(entry, dict): + typ = entry["type"] + entry_without_type = entry.copy() + del entry_without_type["type"] + data.update(entry_without_type) + else: + typ = entry + + zeroconf.setdefault(typ, []).append(data) + + return zeroconf + + +async def async_get_dhcp(hass: HomeAssistant) -> list[dict[str, str]]: + """Return cached list of dhcp types.""" + dhcp: list[dict[str, str]] = DHCP.copy() + + integrations = await async_get_custom_components(hass) + for integration in integrations.values(): + if not integration.dhcp: + continue + for entry in integration.dhcp: + dhcp.append({"domain": integration.domain, **entry}) + + return dhcp + + +async def async_get_homekit(hass: HomeAssistant) -> dict[str, str]: + """Return cached list of homekit models.""" + + homekit: dict[str, str] = HOMEKIT.copy() + + integrations = await async_get_custom_components(hass) + for integration in integrations.values(): + if ( + not integration.homekit + or "models" not in integration.homekit + or not integration.homekit["models"] + ): + continue + for model in integration.homekit["models"]: + homekit[model] = integration.domain + + return homekit + + +async def async_get_ssdp(hass: HomeAssistant) -> dict[str, list[dict[str, str]]]: + """Return cached list of ssdp mappings.""" + + ssdp: dict[str, list[dict[str, str]]] = SSDP.copy() + + integrations = await async_get_custom_components(hass) + for integration in integrations.values(): + if not integration.ssdp: + continue + + ssdp[integration.domain] = integration.ssdp + + return ssdp + + +async def async_get_mqtt(hass: HomeAssistant) -> dict[str, list[str]]: + """Return cached list of MQTT mappings.""" + + mqtt: dict[str, list[str]] = MQTT.copy() + + integrations = await async_get_custom_components(hass) + for integration in integrations.values(): + if not integration.mqtt: + continue + + mqtt[integration.domain] = integration.mqtt + + return mqtt + + +class Integration: + """An integration in Home Assistant.""" + + @classmethod + def resolve_from_root( + cls, hass: HomeAssistant, root_module: ModuleType, domain: str + ) -> Integration | None: + """Resolve an integration from a root module.""" + for base in root_module.__path__: # type: ignore + manifest_path = pathlib.Path(base) / domain / "manifest.json" + + if not manifest_path.is_file(): + continue + + try: + manifest = json.loads(manifest_path.read_text()) + except ValueError as err: + _LOGGER.error( + "Error parsing manifest.json file at %s: %s", manifest_path, err + ) + continue + + return cls( + hass, + f"{root_module.__name__}.{domain}", + manifest_path.parent, + manifest, + ) + + return None + + def __init__( + self, + hass: HomeAssistant, + pkg_path: str, + file_path: pathlib.Path, + manifest: Manifest, + ): + """Initialize an integration.""" + self.hass = hass + self.pkg_path = pkg_path + self.file_path = file_path + self.manifest = manifest + manifest["is_built_in"] = self.is_built_in + + if self.dependencies: + self._all_dependencies_resolved: bool | None = None + self._all_dependencies: set[str] | None = None + else: + self._all_dependencies_resolved = True + self._all_dependencies = set() + + _LOGGER.info("Loaded %s from %s", self.domain, pkg_path) + + @property + def name(self) -> str: + """Return name.""" + return self.manifest["name"] + + @property + def disabled(self) -> str | None: + """Return reason integration is disabled.""" + return self.manifest.get("disabled") + + @property + def domain(self) -> str: + """Return domain.""" + return self.manifest["domain"] + + @property + def dependencies(self) -> list[str]: + """Return dependencies.""" + return self.manifest.get("dependencies", []) + + @property + def after_dependencies(self) -> list[str]: + """Return after_dependencies.""" + return self.manifest.get("after_dependencies", []) + + @property + def requirements(self) -> list[str]: + """Return requirements.""" + return self.manifest.get("requirements", []) + + @property + def config_flow(self) -> bool: + """Return config_flow.""" + return self.manifest.get("config_flow") or False + + @property + def documentation(self) -> str | None: + """Return documentation.""" + return self.manifest.get("documentation") + + @property + def issue_tracker(self) -> str | None: + """Return issue tracker link.""" + return self.manifest.get("issue_tracker") + + @property + def quality_scale(self) -> str | None: + """Return Integration Quality Scale.""" + return self.manifest.get("quality_scale") + + @property + def iot_class(self) -> str | None: + """Return the integration IoT Class.""" + return self.manifest.get("iot_class") + + @property + def mqtt(self) -> list[str] | None: + """Return Integration MQTT entries.""" + return self.manifest.get("mqtt") + + @property + def ssdp(self) -> list[dict[str, str]] | None: + """Return Integration SSDP entries.""" + return self.manifest.get("ssdp") + + @property + def zeroconf(self) -> list[str | dict[str, str]] | None: + """Return Integration zeroconf entries.""" + return self.manifest.get("zeroconf") + + @property + def dhcp(self) -> list[dict[str, str]] | None: + """Return Integration dhcp entries.""" + return self.manifest.get("dhcp") + + @property + def homekit(self) -> dict[str, list[str]] | None: + """Return Integration homekit entries.""" + return self.manifest.get("homekit") + + @property + def is_built_in(self) -> bool: + """Test if package is a built-in integration.""" + return self.pkg_path.startswith(PACKAGE_BUILTIN) + + @property + def version(self) -> AwesomeVersion | None: + """Return the version of the integration.""" + if "version" not in self.manifest: + return None + return AwesomeVersion(self.manifest["version"]) + + @property + def all_dependencies(self) -> set[str]: + """Return all dependencies including sub-dependencies.""" + if self._all_dependencies is None: + raise RuntimeError("Dependencies not resolved!") + + return self._all_dependencies + + @property + def all_dependencies_resolved(self) -> bool: + """Return if all dependencies have been resolved.""" + return self._all_dependencies_resolved is not None + + async def resolve_dependencies(self) -> bool: + """Resolve all dependencies.""" + if self._all_dependencies_resolved is not None: + return self._all_dependencies_resolved + + try: + dependencies = await _async_component_dependencies( + self.hass, self.domain, self, set(), set() + ) + dependencies.discard(self.domain) + self._all_dependencies = dependencies + self._all_dependencies_resolved = True + except IntegrationNotFound as err: + _LOGGER.error( + "Unable to resolve dependencies for %s: we are unable to resolve (sub)dependency %s", + self.domain, + err.domain, + ) + self._all_dependencies_resolved = False + except CircularDependency as err: + _LOGGER.error( + "Unable to resolve dependencies for %s: it contains a circular dependency: %s -> %s", + self.domain, + err.from_domain, + err.to_domain, + ) + self._all_dependencies_resolved = False + + return self._all_dependencies_resolved + + def get_component(self) -> ModuleType: + """Return the component.""" + cache = self.hass.data.setdefault(DATA_COMPONENTS, {}) + if self.domain not in cache: + cache[self.domain] = importlib.import_module(self.pkg_path) + return cache[self.domain] # type: ignore + + def get_platform(self, platform_name: str) -> ModuleType: + """Return a platform for an integration.""" + cache = self.hass.data.setdefault(DATA_COMPONENTS, {}) + full_name = f"{self.domain}.{platform_name}" + if full_name not in cache: + cache[full_name] = self._import_platform(platform_name) + return cache[full_name] # type: ignore + + def _import_platform(self, platform_name: str) -> ModuleType: + """Import the platform.""" + return importlib.import_module(f"{self.pkg_path}.{platform_name}") + + def __repr__(self) -> str: + """Text representation of class.""" + return f"" + + +async def async_get_integration(hass: HomeAssistant, domain: str) -> Integration: + """Get an integration.""" + cache = hass.data.get(DATA_INTEGRATIONS) + if cache is None: + if not _async_mount_config_dir(hass): + raise IntegrationNotFound(domain) + cache = hass.data[DATA_INTEGRATIONS] = {} + + int_or_evt: Integration | asyncio.Event | None = cache.get(domain, _UNDEF) + + if isinstance(int_or_evt, asyncio.Event): + await int_or_evt.wait() + int_or_evt = cache.get(domain, _UNDEF) + + # When we have waited and it's _UNDEF, it doesn't exist + # We don't cache that it doesn't exist, or else people can't fix it + # and then restart, because their config will never be valid. + if int_or_evt is _UNDEF: + raise IntegrationNotFound(domain) + + if int_or_evt is not _UNDEF: + return cast(Integration, int_or_evt) + + event = cache[domain] = asyncio.Event() + + try: + integration = await _async_get_integration(hass, domain) + except Exception: # pylint: disable=broad-except + # Remove event from cache. + cache.pop(domain) + event.set() + raise + + cache[domain] = integration + event.set() + return integration + + +async def _async_get_integration(hass: HomeAssistant, domain: str) -> Integration: + # Instead of using resolve_from_root we use the cache of custom + # components to find the integration. + if integration := (await async_get_custom_components(hass)).get(domain): + validate_custom_integration_version(integration) + _LOGGER.warning(CUSTOM_WARNING, integration.domain) + return integration + + from homeassistant import components # pylint: disable=import-outside-toplevel + + if integration := await hass.async_add_executor_job( + Integration.resolve_from_root, hass, components, domain + ): + return integration + + raise IntegrationNotFound(domain) + + +class LoaderError(Exception): + """Loader base error.""" + + +class IntegrationNotFound(LoaderError): + """Raised when a component is not found.""" + + def __init__(self, domain: str) -> None: + """Initialize a component not found error.""" + super().__init__(f"Integration '{domain}' not found.") + self.domain = domain + + +class CircularDependency(LoaderError): + """Raised when a circular dependency is found when resolving components.""" + + def __init__(self, from_domain: str, to_domain: str) -> None: + """Initialize circular dependency error.""" + super().__init__(f"Circular dependency detected: {from_domain} -> {to_domain}.") + self.from_domain = from_domain + self.to_domain = to_domain + + +def _load_file( + hass: HomeAssistant, comp_or_platform: str, base_paths: list[str] +) -> ModuleType | None: + """Try to load specified file. + + Looks in config dir first, then built-in components. + Only returns it if also found to be valid. + Async friendly. + """ + with suppress(KeyError): + return hass.data[DATA_COMPONENTS][comp_or_platform] # type: ignore + + cache = hass.data.get(DATA_COMPONENTS) + if cache is None: + if not _async_mount_config_dir(hass): + return None + cache = hass.data[DATA_COMPONENTS] = {} + + for path in (f"{base}.{comp_or_platform}" for base in base_paths): + try: + module = importlib.import_module(path) + + # In Python 3 you can import files from directories that do not + # contain the file __init__.py. A directory is a valid module if + # it contains a file with the .py extension. In this case Python + # will succeed in importing the directory as a module and call it + # a namespace. We do not care about namespaces. + # This prevents that when only + # custom_components/switch/some_platform.py exists, + # the import custom_components.switch would succeed. + # __file__ was unset for namespaces before Python 3.7 + if getattr(module, "__file__", None) is None: + continue + + cache[comp_or_platform] = module + + return module + + except ImportError as err: + # This error happens if for example custom_components/switch + # exists and we try to load switch.demo. + # Ignore errors for custom_components, custom_components.switch + # and custom_components.switch.demo. + white_listed_errors = [] + parts = [] + for part in path.split("."): + parts.append(part) + white_listed_errors.append(f"No module named '{'.'.join(parts)}'") + + if str(err) not in white_listed_errors: + _LOGGER.exception( + ("Error loading %s. Make sure all dependencies are installed"), path + ) + + return None + + +class ModuleWrapper: + """Class to wrap a Python module and auto fill in hass argument.""" + + def __init__(self, hass: HomeAssistant, module: ModuleType) -> None: + """Initialize the module wrapper.""" + self._hass = hass + self._module = module + + def __getattr__(self, attr: str) -> Any: + """Fetch an attribute.""" + value = getattr(self._module, attr) + + if hasattr(value, "__bind_hass"): + value = ft.partial(value, self._hass) + + setattr(self, attr, value) + return value + + +class Components: + """Helper to load components.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the Components class.""" + self._hass = hass + + def __getattr__(self, comp_name: str) -> ModuleWrapper: + """Fetch a component.""" + # Test integration cache + integration = self._hass.data.get(DATA_INTEGRATIONS, {}).get(comp_name) + + if isinstance(integration, Integration): + component: ModuleType | None = integration.get_component() + else: + # Fallback to importing old-school + component = _load_file(self._hass, comp_name, _lookup_path(self._hass)) + + if component is None: + raise ImportError(f"Unable to load {comp_name}") + + wrapped = ModuleWrapper(self._hass, component) + setattr(self, comp_name, wrapped) + return wrapped + + +class Helpers: + """Helper to load helpers.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the Helpers class.""" + self._hass = hass + + def __getattr__(self, helper_name: str) -> ModuleWrapper: + """Fetch a helper.""" + helper = importlib.import_module(f"homeassistant.helpers.{helper_name}") + wrapped = ModuleWrapper(self._hass, helper) + setattr(self, helper_name, wrapped) + return wrapped + + +def bind_hass(func: CALLABLE_T) -> CALLABLE_T: + """Decorate function to indicate that first argument is hass.""" + setattr(func, "__bind_hass", True) + return func + + +async def _async_component_dependencies( + hass: HomeAssistant, + start_domain: str, + integration: Integration, + loaded: set[str], + loading: set[str], +) -> set[str]: + """Recursive function to get component dependencies. + + Async friendly. + """ + domain = integration.domain + loading.add(domain) + + for dependency_domain in integration.dependencies: + # Check not already loaded + if dependency_domain in loaded: + continue + + # If we are already loading it, we have a circular dependency. + if dependency_domain in loading: + raise CircularDependency(domain, dependency_domain) + + loaded.add(dependency_domain) + + dep_integration = await async_get_integration(hass, dependency_domain) + + if start_domain in dep_integration.after_dependencies: + raise CircularDependency(start_domain, dependency_domain) + + if dep_integration.dependencies: + dep_loaded = await _async_component_dependencies( + hass, start_domain, dep_integration, loaded, loading + ) + + loaded.update(dep_loaded) + + loaded.add(domain) + loading.remove(domain) + + return loaded + + +def _async_mount_config_dir(hass: HomeAssistant) -> bool: + """Mount config dir in order to load custom_component. + + Async friendly but not a coroutine. + """ + if hass.config.config_dir is None: + _LOGGER.error("Can't load integrations - configuration directory is not set") + return False + if hass.config.config_dir not in sys.path: + sys.path.insert(0, hass.config.config_dir) + return True + + +def _lookup_path(hass: HomeAssistant) -> list[str]: + """Return the lookup paths for legacy lookups.""" + if hass.config.safe_mode: + return [PACKAGE_BUILTIN] + return [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN] + + +def validate_custom_integration_version(integration: Integration) -> None: + """ + Validate the version of custom integrations. + + Raises IntegrationNotFound when version is missing or not valid + """ + try: + AwesomeVersion( + integration.version, + [ + AwesomeVersionStrategy.CALVER, + AwesomeVersionStrategy.SEMVER, + AwesomeVersionStrategy.SIMPLEVER, + AwesomeVersionStrategy.BUILDVER, + AwesomeVersionStrategy.PEP440, + ], + ) + except AwesomeVersionException: + _LOGGER.error( + "The custom integration '%s' does not have a " + "valid version key (%s) in the manifest file and was blocked from loading. " + "See https://developers.home-assistant.io/blog/2021/01/29/custom-integration-changes#versions for more details", + integration.domain, + integration.version, + ) + raise IntegrationNotFound(integration.domain) from None diff --git a/homeassistant-2021.6.0.dev0/homeassistant/package_constraints.txt b/homeassistant-2021.6.0.dev0/homeassistant/package_constraints.txt new file mode 100644 index 00000000000..9755a5b2e5d --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/package_constraints.txt @@ -0,0 +1,61 @@ +PyJWT==1.7.1 +PyNaCl==1.3.0 +aiodiscover==1.4.0 +aiohttp==3.7.4.post0 +aiohttp_cors==0.7.0 +astral==2.2 +async-upnp-client==0.17.0 +async_timeout==3.0.1 +attrs==21.2.0 +awesomeversion==21.4.0 +backports.zoneinfo;python_version<"3.9" +bcrypt==3.1.7 +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.43.0 +home-assistant-frontend==20210517.0 +httpx==0.18.0 +jinja2>=2.11.3 +netdisco==2.8.3 +paho-mqtt==1.5.1 +pillow==8.1.2 +pip>=8.0.3,<20.3 +pyroute2==0.5.18 +python-slugify==4.0.1 +pyyaml==5.4.1 +requests==2.25.1 +ruamel.yaml==0.15.100 +scapy==2.4.5 +sqlalchemy==1.4.13 +voluptuous-serialize==2.4.0 +voluptuous==0.12.1 +yarl==1.6.3 +zeroconf==0.31.0 + +pycryptodome>=3.6.6 + +# Constrain urllib3 to ensure we deal with CVE-2019-11236 & CVE-2019-11324 +urllib3>=1.24.3 + +# Constrain H11 to ensure we get a new enough version to support non-rfc line endings +h11>=0.12.0 + +# Constrain httplib2 to protect against GHSA-93xj-8mrv-444m +# https://github.com/advisories/GHSA-93xj-8mrv-444m +httplib2>=0.19.0 + +# This is a old unmaintained library and is replaced with pycryptodome +pycrypto==1000000000.0.0 + +# To remove reliance on typing +btlewrap>=0.0.10 + +# This overrides a built-in Python package +enum34==1000000000.0.0 +typing==1000000000.0.0 +uuid==1000000000.0.0 + diff --git a/homeassistant-2021.6.0.dev0/homeassistant/requirements.py b/homeassistant-2021.6.0.dev0/homeassistant/requirements.py new file mode 100644 index 00000000000..02187fe8f0e --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/requirements.py @@ -0,0 +1,177 @@ +"""Module to handle installing requirements.""" +from __future__ import annotations + +import asyncio +from collections.abc import Iterable +import os +from typing import Any, cast + +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.typing import UNDEFINED, UndefinedType +from homeassistant.loader import Integration, IntegrationNotFound, async_get_integration +import homeassistant.util.package as pkg_util + +# mypy: disallow-any-generics + +DATA_PIP_LOCK = "pip_lock" +DATA_PKG_CACHE = "pkg_cache" +DATA_INTEGRATIONS_WITH_REQS = "integrations_with_reqs" +CONSTRAINT_FILE = "package_constraints.txt" +DISCOVERY_INTEGRATIONS: dict[str, Iterable[str]] = { + "dhcp": ("dhcp",), + "mqtt": ("mqtt",), + "ssdp": ("ssdp",), + "zeroconf": ("zeroconf", "homekit"), +} + + +class RequirementsNotFound(HomeAssistantError): + """Raised when a component is not found.""" + + def __init__(self, domain: str, requirements: list[str]) -> None: + """Initialize a component not found error.""" + super().__init__(f"Requirements for {domain} not found: {requirements}.") + self.domain = domain + self.requirements = requirements + + +async def async_get_integration_with_requirements( + hass: HomeAssistant, domain: str, done: set[str] | None = None +) -> Integration: + """Get an integration with all requirements installed, including the dependencies. + + This can raise IntegrationNotFound if manifest or integration + is invalid, RequirementNotFound if there was some type of + failure to install requirements. + """ + if done is None: + done = {domain} + else: + done.add(domain) + + integration = await async_get_integration(hass, domain) + + if hass.config.skip_pip: + return integration + + cache = hass.data.get(DATA_INTEGRATIONS_WITH_REQS) + if cache is None: + cache = hass.data[DATA_INTEGRATIONS_WITH_REQS] = {} + + int_or_evt: Integration | asyncio.Event | None | UndefinedType = cache.get( + domain, UNDEFINED + ) + + if isinstance(int_or_evt, asyncio.Event): + await int_or_evt.wait() + + int_or_evt = cache.get(domain, UNDEFINED) + + # When we have waited and it's UNDEFINED, it doesn't exist + # We don't cache that it doesn't exist, or else people can't fix it + # and then restart, because their config will never be valid. + if int_or_evt is UNDEFINED: + raise IntegrationNotFound(domain) + + if int_or_evt is not UNDEFINED: + return cast(Integration, int_or_evt) + + event = cache[domain] = asyncio.Event() + + try: + await _async_process_integration(hass, integration, done) + except Exception: + del cache[domain] + event.set() + raise + + cache[domain] = integration + event.set() + return integration + + +async def _async_process_integration( + hass: HomeAssistant, integration: Integration, done: set[str] +) -> None: + """Process an integration and requirements.""" + if integration.requirements: + await async_process_requirements( + hass, integration.domain, integration.requirements + ) + + deps_to_check = [ + dep + for dep in integration.dependencies + integration.after_dependencies + if dep not in done + ] + + for check_domain, to_check in DISCOVERY_INTEGRATIONS.items(): + if ( + check_domain not in done + and check_domain not in deps_to_check + and any(check in integration.manifest for check in to_check) + ): + deps_to_check.append(check_domain) + + if not deps_to_check: + return + + results = await asyncio.gather( + *[ + async_get_integration_with_requirements(hass, dep, done) + for dep in deps_to_check + ], + return_exceptions=True, + ) + for result in results: + if not isinstance(result, BaseException): + continue + if not isinstance(result, IntegrationNotFound) or not ( + not integration.is_built_in + and result.domain in integration.after_dependencies + ): + raise result + + +async def async_process_requirements( + hass: HomeAssistant, name: str, requirements: list[str] +) -> None: + """Install the requirements for a component or platform. + + This method is a coroutine. It will raise RequirementsNotFound + if an requirement can't be satisfied. + """ + pip_lock = hass.data.get(DATA_PIP_LOCK) + if pip_lock is None: + pip_lock = hass.data[DATA_PIP_LOCK] = asyncio.Lock() + + kwargs = pip_kwargs(hass.config.config_dir) + + async with pip_lock: + for req in requirements: + if pkg_util.is_installed(req): + continue + + def _install(req: str, kwargs: dict[str, Any]) -> bool: + """Install requirement.""" + return pkg_util.install_package(req, **kwargs) + + ret = await hass.async_add_executor_job(_install, req, kwargs) + + if not ret: + raise RequirementsNotFound(name, [req]) + + +def pip_kwargs(config_dir: str | None) -> dict[str, Any]: + """Return keyword arguments for PIP install.""" + is_docker = pkg_util.is_docker_env() + kwargs = { + "constraints": os.path.join(os.path.dirname(__file__), CONSTRAINT_FILE), + "no_cache_dir": is_docker, + } + if "WHEELS_LINKS" in os.environ: + kwargs["find_links"] = os.environ["WHEELS_LINKS"] + if not (config_dir is None or pkg_util.is_virtual_env()) and not is_docker: + kwargs["target"] = os.path.join(config_dir, "deps") + return kwargs diff --git a/homeassistant-2021.6.0.dev0/homeassistant/runner.py b/homeassistant-2021.6.0.dev0/homeassistant/runner.py new file mode 100644 index 00000000000..86bebecb7b1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/runner.py @@ -0,0 +1,118 @@ +"""Run Home Assistant.""" +from __future__ import annotations + +import asyncio +import dataclasses +import logging +import threading +from typing import Any + +from homeassistant import bootstrap +from homeassistant.core import callback +from homeassistant.helpers.frame import warn_use +from homeassistant.util.executor import InterruptibleThreadPoolExecutor +from homeassistant.util.thread import deadlock_safe_shutdown + +# mypy: disallow-any-generics + +# +# Python 3.8 has significantly less workers by default +# than Python 3.7. In order to be consistent between +# supported versions, we need to set max_workers. +# +# In most cases the workers are not I/O bound, as they +# are sleeping/blocking waiting for data from integrations +# updating so this number should be higher than the default +# use case. +# +MAX_EXECUTOR_WORKERS = 64 + + +@dataclasses.dataclass +class RuntimeConfig: + """Class to hold the information for running Home Assistant.""" + + config_dir: str + skip_pip: bool = False + safe_mode: bool = False + + verbose: bool = False + + log_rotate_days: int | None = None + log_file: str | None = None + log_no_color: bool = False + + debug: bool = False + open_ui: bool = False + + +class HassEventLoopPolicy(asyncio.DefaultEventLoopPolicy): # type: ignore[valid-type,misc] + """Event loop policy for Home Assistant.""" + + def __init__(self, debug: bool) -> None: + """Init the event loop policy.""" + super().__init__() + self.debug = debug + + @property + def loop_name(self) -> str: + """Return name of the loop.""" + return self._loop_factory.__name__ # type: ignore + + def new_event_loop(self) -> asyncio.AbstractEventLoop: + """Get the event loop.""" + loop: asyncio.AbstractEventLoop = super().new_event_loop() + loop.set_exception_handler(_async_loop_exception_handler) + if self.debug: + loop.set_debug(True) + + executor = InterruptibleThreadPoolExecutor( + thread_name_prefix="SyncWorker", max_workers=MAX_EXECUTOR_WORKERS + ) + loop.set_default_executor(executor) + loop.set_default_executor = warn_use( # type: ignore + loop.set_default_executor, "sets default executor on the event loop" + ) + + # Shut down executor when we shut down loop + orig_close = loop.close + + def close() -> None: + executor.logged_shutdown() + orig_close() + + loop.close = close # type: ignore + + return loop + + +@callback +def _async_loop_exception_handler(_: Any, context: dict[str, Any]) -> None: + """Handle all exception inside the core loop.""" + kwargs = {} + exception = context.get("exception") + if exception: + kwargs["exc_info"] = (type(exception), exception, exception.__traceback__) + + logging.getLogger(__package__).error( + "Error doing job: %s", context["message"], **kwargs # type: ignore + ) + + +async def setup_and_run_hass(runtime_config: RuntimeConfig) -> int: + """Set up Home Assistant and run.""" + hass = await bootstrap.async_setup_hass(runtime_config) + + if hass is None: + return 1 + + # threading._shutdown can deadlock forever + threading._shutdown = deadlock_safe_shutdown # type: ignore[attr-defined] # pylint: disable=protected-access + + return await hass.async_run() + + +def run(runtime_config: RuntimeConfig) -> int: + """Run Home Assistant.""" + asyncio.set_event_loop_policy(HassEventLoopPolicy(runtime_config.debug)) + return asyncio.run(setup_and_run_hass(runtime_config)) diff --git a/homeassistant-2021.6.0.dev0/homeassistant/setup.py b/homeassistant-2021.6.0.dev0/homeassistant/setup.py new file mode 100644 index 00000000000..f5a6f9b9721 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/setup.py @@ -0,0 +1,479 @@ +"""All methods needed to bootstrap a Home Assistant instance.""" +from __future__ import annotations + +import asyncio +from collections.abc import Awaitable, Generator, Iterable +import contextlib +import logging.handlers +from timeit import default_timer as timer +from types import ModuleType +from typing import Callable + +from homeassistant import config as conf_util, core, loader, requirements +from homeassistant.config import async_notify_setup_error +from homeassistant.const import ( + EVENT_COMPONENT_LOADED, + EVENT_HOMEASSISTANT_START, + PLATFORM_FORMAT, +) +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.typing import ConfigType +from homeassistant.util import dt as dt_util, ensure_unique_string + +_LOGGER = logging.getLogger(__name__) + +ATTR_COMPONENT = "component" + +BASE_PLATFORMS = { + "air_quality", + "alarm_control_panel", + "binary_sensor", + "camera", + "climate", + "cover", + "device_tracker", + "fan", + "humidifier", + "image_processing", + "light", + "lock", + "media_player", + "notify", + "remote", + "scene", + "sensor", + "switch", + "tts", + "vacuum", + "water_heater", +} + +DATA_SETUP_DONE = "setup_done" +DATA_SETUP_STARTED = "setup_started" +DATA_SETUP_TIME = "setup_time" + +DATA_SETUP = "setup_tasks" +DATA_DEPS_REQS = "deps_reqs_processed" + +SLOW_SETUP_WARNING = 10 +SLOW_SETUP_MAX_WAIT = 300 + + +@core.callback +def async_set_domains_to_be_loaded(hass: core.HomeAssistant, domains: set[str]) -> None: + """Set domains that are going to be loaded from the config. + + This will allow us to properly handle after_dependencies. + """ + hass.data[DATA_SETUP_DONE] = {domain: asyncio.Event() for domain in domains} + + +def setup_component(hass: core.HomeAssistant, domain: str, config: ConfigType) -> bool: + """Set up a component and all its dependencies.""" + return asyncio.run_coroutine_threadsafe( + async_setup_component(hass, domain, config), hass.loop + ).result() + + +async def async_setup_component( + hass: core.HomeAssistant, domain: str, config: ConfigType +) -> bool: + """Set up a component and all its dependencies. + + This method is a coroutine. + """ + if domain in hass.config.components: + return True + + setup_tasks = hass.data.setdefault(DATA_SETUP, {}) + + if domain in setup_tasks: + return await setup_tasks[domain] # type: ignore + + task = setup_tasks[domain] = hass.async_create_task( + _async_setup_component(hass, domain, config) + ) + + try: + return await task # type: ignore + finally: + if domain in hass.data.get(DATA_SETUP_DONE, {}): + hass.data[DATA_SETUP_DONE].pop(domain).set() + + +async def _async_process_dependencies( + hass: core.HomeAssistant, config: ConfigType, integration: loader.Integration +) -> bool: + """Ensure all dependencies are set up.""" + dependencies_tasks = { + dep: hass.loop.create_task(async_setup_component(hass, dep, config)) + for dep in integration.dependencies + if dep not in hass.config.components + } + + after_dependencies_tasks = {} + to_be_loaded = hass.data.get(DATA_SETUP_DONE, {}) + for dep in integration.after_dependencies: + if ( + dep not in dependencies_tasks + and dep in to_be_loaded + and dep not in hass.config.components + ): + after_dependencies_tasks[dep] = hass.loop.create_task( + to_be_loaded[dep].wait() + ) + + if not dependencies_tasks and not after_dependencies_tasks: + return True + + if dependencies_tasks: + _LOGGER.debug( + "Dependency %s will wait for dependencies %s", + integration.domain, + list(dependencies_tasks), + ) + if after_dependencies_tasks: + _LOGGER.debug( + "Dependency %s will wait for after dependencies %s", + integration.domain, + list(after_dependencies_tasks), + ) + + async with hass.timeout.async_freeze(integration.domain): + results = await asyncio.gather( + *dependencies_tasks.values(), *after_dependencies_tasks.values() + ) + + failed = [ + domain for idx, domain in enumerate(dependencies_tasks) if not results[idx] + ] + + if failed: + _LOGGER.error( + "Unable to set up dependencies of %s. Setup failed for dependencies: %s", + integration.domain, + ", ".join(failed), + ) + + return False + return True + + +async def _async_setup_component( + hass: core.HomeAssistant, domain: str, config: ConfigType +) -> bool: + """Set up a component for Home Assistant. + + This method is a coroutine. + """ + + def log_error(msg: str, link: str | None = None) -> None: + """Log helper.""" + _LOGGER.error("Setup failed for %s: %s", domain, msg) + async_notify_setup_error(hass, domain, link) + + try: + integration = await loader.async_get_integration(hass, domain) + except loader.IntegrationNotFound: + log_error("Integration not found.") + return False + + if integration.disabled: + log_error(f"Dependency is disabled - {integration.disabled}") + return False + + # Validate all dependencies exist and there are no circular dependencies + if not await integration.resolve_dependencies(): + return False + + # Process requirements as soon as possible, so we can import the component + # without requiring imports to be in functions. + try: + await async_process_deps_reqs(hass, config, integration) + except HomeAssistantError as err: + log_error(str(err), integration.documentation) + return False + + # Some integrations fail on import because they call functions incorrectly. + # So we do it before validating config to catch these errors. + try: + component = integration.get_component() + except ImportError as err: + log_error(f"Unable to import component: {err}", integration.documentation) + return False + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Setup failed for %s: unknown error", domain) + return False + + processed_config = await conf_util.async_process_component_config( + hass, config, integration + ) + + if processed_config is None: + log_error("Invalid config.", integration.documentation) + return False + + start = timer() + _LOGGER.info("Setting up %s", domain) + with async_start_setup(hass, [domain]): + if hasattr(component, "PLATFORM_SCHEMA"): + # Entity components have their own warning + warn_task = None + else: + warn_task = hass.loop.call_later( + SLOW_SETUP_WARNING, + _LOGGER.warning, + "Setup of %s is taking over %s seconds.", + domain, + SLOW_SETUP_WARNING, + ) + + task = None + result = True + try: + if hasattr(component, "async_setup"): + task = component.async_setup(hass, processed_config) # type: ignore + elif hasattr(component, "setup"): + # This should not be replaced with hass.async_add_executor_job because + # we don't want to track this task in case it blocks startup. + task = hass.loop.run_in_executor( + None, component.setup, hass, processed_config # type: ignore + ) + elif not hasattr(component, "async_setup_entry"): + log_error("No setup or config entry setup function defined.") + return False + + if task: + async with hass.timeout.async_timeout(SLOW_SETUP_MAX_WAIT, domain): + result = await task + except asyncio.TimeoutError: + _LOGGER.error( + "Setup of %s is taking longer than %s seconds." + " Startup will proceed without waiting any longer", + domain, + SLOW_SETUP_MAX_WAIT, + ) + return False + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error during setup of component %s", domain) + async_notify_setup_error(hass, domain, integration.documentation) + return False + finally: + end = timer() + if warn_task: + warn_task.cancel() + _LOGGER.info("Setup of domain %s took %.1f seconds", domain, end - start) + + if result is False: + log_error("Integration failed to initialize.") + return False + if result is not True: + log_error( + f"Integration {domain!r} did not return boolean if setup was " + "successful. Disabling component." + ) + return False + + # Flush out async_setup calling create_task. Fragile but covered by test. + await asyncio.sleep(0) + await hass.config_entries.flow.async_wait_init_flow_finish(domain) + + await asyncio.gather( + *[ + entry.async_setup(hass, integration=integration) + for entry in hass.config_entries.async_entries(domain) + ] + ) + + hass.config.components.add(domain) + + # Cleanup + if domain in hass.data[DATA_SETUP]: + hass.data[DATA_SETUP].pop(domain) + + hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: domain}) + + return True + + +async def async_prepare_setup_platform( + hass: core.HomeAssistant, hass_config: ConfigType, domain: str, platform_name: str +) -> ModuleType | None: + """Load a platform and makes sure dependencies are setup. + + This method is a coroutine. + """ + platform_path = PLATFORM_FORMAT.format(domain=domain, platform=platform_name) + + def log_error(msg: str) -> None: + """Log helper.""" + _LOGGER.error("Unable to prepare setup for platform %s: %s", platform_path, msg) + async_notify_setup_error(hass, platform_path) + + try: + integration = await loader.async_get_integration(hass, platform_name) + except loader.IntegrationNotFound: + log_error("Integration not found") + return None + + # Process deps and reqs as soon as possible, so that requirements are + # available when we import the platform. + try: + await async_process_deps_reqs(hass, hass_config, integration) + except HomeAssistantError as err: + log_error(str(err)) + return None + + try: + platform = integration.get_platform(domain) + except ImportError as exc: + log_error(f"Platform not found ({exc}).") + return None + + # Already loaded + if platform_path in hass.config.components: + return platform + + # Platforms cannot exist on their own, they are part of their integration. + # If the integration is not set up yet, and can be set up, set it up. + if integration.domain not in hass.config.components: + try: + component = integration.get_component() + except ImportError as exc: + log_error(f"Unable to import the component ({exc}).") + return None + + if ( + hasattr(component, "setup") or hasattr(component, "async_setup") + ) and not await async_setup_component(hass, integration.domain, hass_config): + log_error("Unable to set up component.") + return None + + return platform + + +async def async_process_deps_reqs( + hass: core.HomeAssistant, config: ConfigType, integration: loader.Integration +) -> None: + """Process all dependencies and requirements for a module. + + Module is a Python module of either a component or platform. + """ + processed = hass.data.get(DATA_DEPS_REQS) + + if processed is None: + processed = hass.data[DATA_DEPS_REQS] = set() + elif integration.domain in processed: + return + + if not await _async_process_dependencies(hass, config, integration): + raise HomeAssistantError("Could not set up all dependencies.") + + if not hass.config.skip_pip and integration.requirements: + async with hass.timeout.async_freeze(integration.domain): + await requirements.async_get_integration_with_requirements( + hass, integration.domain + ) + + processed.add(integration.domain) + + +@core.callback +def async_when_setup( + hass: core.HomeAssistant, + component: str, + when_setup_cb: Callable[[core.HomeAssistant, str], Awaitable[None]], +) -> None: + """Call a method when a component is setup.""" + _async_when_setup(hass, component, when_setup_cb, False) + + +@core.callback +def async_when_setup_or_start( + hass: core.HomeAssistant, + component: str, + when_setup_cb: Callable[[core.HomeAssistant, str], Awaitable[None]], +) -> None: + """Call a method when a component is setup or state is fired.""" + _async_when_setup(hass, component, when_setup_cb, True) + + +@core.callback +def _async_when_setup( + hass: core.HomeAssistant, + component: str, + when_setup_cb: Callable[[core.HomeAssistant, str], Awaitable[None]], + start_event: bool, +) -> None: + """Call a method when a component is setup or the start event fires.""" + + async def when_setup() -> None: + """Call the callback.""" + try: + await when_setup_cb(hass, component) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error handling when_setup callback for %s", component) + + if component in hass.config.components: + hass.async_create_task(when_setup()) + return + + listeners: list[Callable] = [] + + async def _matched_event(event: core.Event) -> None: + """Call the callback when we matched an event.""" + for listener in listeners: + listener() + await when_setup() + + async def _loaded_event(event: core.Event) -> None: + """Call the callback if we loaded the expected component.""" + if event.data[ATTR_COMPONENT] == component: + await _matched_event(event) + + listeners.append(hass.bus.async_listen(EVENT_COMPONENT_LOADED, _loaded_event)) + if start_event: + listeners.append( + hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _matched_event) + ) + + +@core.callback +def async_get_loaded_integrations(hass: core.HomeAssistant) -> set: + """Return the complete list of loaded integrations.""" + integrations = set() + for component in hass.config.components: + if "." not in component: + integrations.add(component) + continue + domain, platform = component.split(".", 1) + if domain in BASE_PLATFORMS: + integrations.add(platform) + return integrations + + +@contextlib.contextmanager +def async_start_setup(hass: core.HomeAssistant, components: Iterable) -> Generator: + """Keep track of when setup starts and finishes.""" + setup_started = hass.data.setdefault(DATA_SETUP_STARTED, {}) + started = dt_util.utcnow() + unique_components = {} + for domain in components: + unique = ensure_unique_string(domain, setup_started) + unique_components[unique] = domain + setup_started[unique] = started + + yield + + setup_time = hass.data.setdefault(DATA_SETUP_TIME, {}) + time_taken = dt_util.utcnow() - started + for unique, domain in unique_components.items(): + del setup_started[unique] + if "." in domain: + _, integration = domain.split(".", 1) + else: + integration = domain + if integration in setup_time: + setup_time[integration] += time_taken + else: + setup_time[integration] = time_taken diff --git a/homeassistant-2021.6.0.dev0/homeassistant/strings.json b/homeassistant-2021.6.0.dev0/homeassistant/strings.json new file mode 100644 index 00000000000..31693c5bba1 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/homeassistant/strings.json @@ -0,0 +1,79 @@ +{ + "common": { + "state": { + "off": "Off", + "on": "On", + "open": "Open", + "closed": "Closed", + "connected": "Connected", + "disconnected": "Disconnected", + "locked": "Locked", + "unlocked": "Unlocked", + "active": "Active", + "idle": "Idle", + "standby": "Standby", + "paused": "Paused", + "home": "Home", + "not_home": "Away" + }, + "config_flow": { + "title": { + "oauth2_pick_implementation": "Pick Authentication Method", + "reauth": "Reauthenticate Integration", + "via_hassio_addon": "{name} via Home Assistant add-on" + }, + "description": { + "confirm_setup": "Do you want to start set up?" + }, + "data": { + "name": "Name", + "email": "Email", + "username": "Username", + "password": "Password", + "host": "Host", + "ip": "IP Address", + "port": "Port", + "url": "URL", + "usb_path": "USB Device Path", + "access_token": "Access Token", + "api_key": "API Key", + "api_token": "API Token", + "ssl": "Uses an SSL certificate", + "verify_ssl": "Verify SSL certificate", + "elevation": "Elevation", + "longitude": "Longitude", + "latitude": "Latitude", + "location": "Location", + "pin": "PIN Code", + "mode": "Mode" + }, + "create_entry": { + "authenticated": "Successfully authenticated" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_access_token": "Invalid access token", + "invalid_api_key": "Invalid API key", + "invalid_auth": "Invalid authentication", + "invalid_host": "Invalid hostname or IP address", + "unknown": "Unexpected error" + }, + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible.", + "already_configured_account": "Account is already configured", + "already_configured_device": "Device is already configured", + "already_configured_location": "Location is already configured", + "already_configured_service": "Service is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network", + "webhook_not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive webhook messages.", + "oauth2_error": "Received invalid token data.", + "oauth2_missing_configuration": "The component is not configured. Please follow the documentation.", + "oauth2_authorize_url_timeout": "Timeout generating authorize URL.", + "oauth2_no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", + "reauth_successful": "Re-authentication was successful", + "unknown_authorize_url_generation": "Unknown error generating an authorize URL." + } + } + } +} diff --git a/homeassistant-2021.6.0.dev0/pyproject.toml b/homeassistant-2021.6.0.dev0/pyproject.toml new file mode 100644 index 00000000000..0e38a197319 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/pyproject.toml @@ -0,0 +1,124 @@ +[tool.black] +target-version = ["py38"] +exclude = 'generated' + +[tool.isort] +# https://github.com/PyCQA/isort/wiki/isort-Settings +profile = "black" +# will group `import x` and `from x import` of the same module. +force_sort_within_sections = true +known_first_party = [ + "homeassistant", + "tests", +] +forced_separate = [ + "tests", +] +combine_as_imports = true + +[tool.pylint.MASTER] +ignore = [ + "tests", +] +# Use a conservative default here; 2 should speed up most setups and not hurt +# any too bad. Override on command line as appropriate. +jobs = 2 +init-hook='from pylint.config.find_default_config_files import find_default_config_files; from pathlib import Path; import sys; sys.path.append(str(Path(Path(list(find_default_config_files())[0]).parent, "pylint/plugins")))' +load-plugins = [ + "pylint.extensions.typing", + "pylint_strict_informational", + "hass_logger" +] +persistent = false +extension-pkg-whitelist = [ + "ciso8601", + "cv2", +] + +[tool.pylint.BASIC] +class-const-naming-style = "any" +good-names = [ + "_", + "ev", + "ex", + "fp", + "i", + "id", + "j", + "k", + "Run", + "T", +] + +[tool.pylint."MESSAGES CONTROL"] +# Reasons disabled: +# format - handled by black +# locally-disabled - it spams too much +# duplicate-code - unavoidable +# cyclic-import - doesn't test if both import on load +# abstract-class-little-used - prevents from setting right foundation +# unused-argument - generic callbacks and setup methods create a lot of warnings +# too-many-* - are not enforced for the sake of readability +# too-few-* - same as too-many-* +# abstract-method - with intro of async there are always methods missing +# inconsistent-return-statements - doesn't handle raise +# too-many-ancestors - it's too strict. +# wrong-import-order - isort guards this +disable = [ + "format", + "abstract-class-little-used", + "abstract-method", + "cyclic-import", + "duplicate-code", + "inconsistent-return-statements", + "locally-disabled", + "not-context-manager", + "too-few-public-methods", + "too-many-ancestors", + "too-many-arguments", + "too-many-branches", + "too-many-instance-attributes", + "too-many-lines", + "too-many-locals", + "too-many-public-methods", + "too-many-return-statements", + "too-many-statements", + "too-many-boolean-expressions", + "unused-argument", + "wrong-import-order", +] +enable = [ + #"useless-suppression", # temporarily every now and then to clean them up + "use-symbolic-message-instead", +] + +[tool.pylint.REPORTS] +score = false + +[tool.pylint.TYPECHECK] +ignored-classes = [ + "_CountingAttr", # for attrs +] + +[tool.pylint.FORMAT] +expected-line-ending-format = "LF" + +[tool.pylint.EXCEPTIONS] +overgeneral-exceptions = [ + "BaseException", + "Exception", + "HomeAssistantError", +] + +[tool.pylint.TYPING] +py-version = "3.8" +runtime-typing = false + +[tool.pytest.ini_options] +testpaths = [ + "tests", +] +norecursedirs = [ + ".git", + "testing_config", +] diff --git a/homeassistant-2021.6.0.dev0/setup.cfg b/homeassistant-2021.6.0.dev0/setup.cfg new file mode 100644 index 00000000000..ad1e6650a59 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/setup.cfg @@ -0,0 +1,34 @@ +[metadata] +license = Apache-2.0 +license_file = LICENSE.md +platforms = any +description = Open-source home automation platform running on Python 3. +long_description = file: README.rst +keywords = home, automation +classifier = + Development Status :: 4 - Beta + Intended Audience :: End Users/Desktop + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Operating System :: OS Independent + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Topic :: Home Automation + +[flake8] +exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build +max-complexity = 25 +doctests = True +# To work with Black +# E501: line too long +# W503: Line break occurred before a binary operator +# E203: Whitespace before ':' +# D202 No blank lines allowed after function docstring +# W504 line break after binary operator +ignore = + E501, + W503, + E203, + D202, + W504 +noqa-require-code = True diff --git a/homeassistant-2021.6.0.dev0/setup.py b/homeassistant-2021.6.0.dev0/setup.py new file mode 100644 index 00000000000..d987f4671b4 --- /dev/null +++ b/homeassistant-2021.6.0.dev0/setup.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""Home Assistant setup script.""" +from datetime import datetime as dt + +from setuptools import find_packages, setup + +import homeassistant.const as hass_const + +PROJECT_NAME = "Home Assistant" +PROJECT_PACKAGE_NAME = "homeassistant" +PROJECT_LICENSE = "Apache License 2.0" +PROJECT_AUTHOR = "The Home Assistant Authors" +PROJECT_COPYRIGHT = f" 2013-{dt.now().year}, {PROJECT_AUTHOR}" +PROJECT_URL = "https://www.home-assistant.io/" +PROJECT_EMAIL = "hello@home-assistant.io" + +PROJECT_GITHUB_USERNAME = "home-assistant" +PROJECT_GITHUB_REPOSITORY = "core" + +PYPI_URL = f"https://pypi.python.org/pypi/{PROJECT_PACKAGE_NAME}" +GITHUB_PATH = f"{PROJECT_GITHUB_USERNAME}/{PROJECT_GITHUB_REPOSITORY}" +GITHUB_URL = f"https://github.com/{GITHUB_PATH}" + +DOWNLOAD_URL = f"{GITHUB_URL}/archive/{hass_const.__version__}.zip" +PROJECT_URLS = { + "Bug Reports": f"{GITHUB_URL}/issues", + "Dev Docs": "https://developers.home-assistant.io/", + "Discord": "https://discordapp.com/invite/c5DvZ4e", + "Forum": "https://community.home-assistant.io/", +} + +PACKAGES = find_packages(exclude=["tests", "tests.*"]) + +REQUIRES = [ + "aiohttp==3.7.4.post0", + "astral==2.2", + "async_timeout==3.0.1", + "attrs==21.2.0", + "awesomeversion==21.4.0", + 'backports.zoneinfo;python_version<"3.9"', + "bcrypt==3.1.7", + "certifi>=2020.12.5", + "ciso8601==2.1.3", + "httpx==0.18.0", + "jinja2>=2.11.3", + "PyJWT==1.7.1", + # PyJWT has loose dependency. We want the latest one. + "cryptography==3.3.2", + "pip>=8.0.3,<20.3", + "python-slugify==4.0.1", + "pyyaml==5.4.1", + "requests==2.25.1", + "ruamel.yaml==0.15.100", + "voluptuous==0.12.1", + "voluptuous-serialize==2.4.0", + "yarl==1.6.3", +] + +MIN_PY_VERSION = ".".join(map(str, hass_const.REQUIRED_PYTHON_VER)) + +setup( + name=PROJECT_PACKAGE_NAME, + version=hass_const.__version__, + url=PROJECT_URL, + download_url=DOWNLOAD_URL, + project_urls=PROJECT_URLS, + author=PROJECT_AUTHOR, + author_email=PROJECT_EMAIL, + packages=PACKAGES, + include_package_data=True, + zip_safe=False, + install_requires=REQUIRES, + python_requires=f">={MIN_PY_VERSION}", + test_suite="tests", + entry_points={"console_scripts": ["hass = homeassistant.__main__:main"]}, +) diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index edff65c545f..9699184382d 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -26,23 +26,25 @@ class WallboxHub: self._username = username self._password = password - def authenticate(self) -> bool: + async def async_authenticate(self, hass) -> bool: """Authenticate using Wallbox API.""" try: wallbox = Wallbox(self._username, self._password) - wallbox.authenticate() + await hass.async_add_executor_job(wallbox.authenticate) return True except requests.exceptions.HTTPError as wallbox_connection_error: if wallbox_connection_error.response.status_code == 403: raise InvalidAuth from wallbox_connection_error raise ConnectionError from wallbox_connection_error - def get_data(self) -> bool: + async def async_get_data(self, hass) -> bool: """Get new sensor data for Wallbox component.""" try: wallbox = Wallbox(self._username, self._password) - wallbox.authenticate() - data = wallbox.getChargerStatus(self._station) + await hass.async_add_executor_job(wallbox.authenticate) + data = await hass.async_add_executor_job( + wallbox.getChargerStatus, self._station + ) return data except requests.exceptions.HTTPError as wallbox_connection_error: if wallbox_connection_error.response.status_code == 403: @@ -63,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): entry.data[CONF_PASSWORD], ) - await hass.async_add_executor_job(wallbox.authenticate) + await wallbox.async_authenticate(hass) hass.data.setdefault(DOMAIN, {CONF_CONNECTIONS: {}}) hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] = wallbox diff --git a/homeassistant/components/wallbox/config_flow.py b/homeassistant/components/wallbox/config_flow.py index e34ce6c532e..d3e99f25b54 100644 --- a/homeassistant/components/wallbox/config_flow.py +++ b/homeassistant/components/wallbox/config_flow.py @@ -25,11 +25,7 @@ async def validate_input(hass: core.HomeAssistant, data): """ hub = WallboxHub(data["station"], data["username"], data["password"]) - await hass.async_add_executor_job( - hub.authenticate, - ) - - await hass.async_add_executor_job(hub.get_data) + await hub.async_get_data(hass) # Return info that you want to store in the config entry. return {"title": "Wallbox Portal"} diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py index 93ff70ddf6a..6fd20713efd 100644 --- a/homeassistant/components/wallbox/const.py +++ b/homeassistant/components/wallbox/const.py @@ -15,80 +15,80 @@ CONF_CONNECTIONS = "connections" SENSOR_TYPES = { "charging_power": { - "ATTR_ICON": "mdi:ev-station", - "ATTR_LABEL": "Charging Power", - "ATTR_ROUND": 2, - "ATTR_UNIT": POWER_KILO_WATT, - "ATTR_ENABLED": True, + "ST_ICON": "mdi:ev-station", + "ST_LABEL": "Charging Power", + "ST_ROUND": 2, + "ST_UNIT": POWER_KILO_WATT, + "ST_ENABLED": True, }, "max_available_power": { - "ATTR_ICON": "mdi:ev-station", - "ATTR_LABEL": "Max Available Power", - "ATTR_ROUND": 0, - "ATTR_UNIT": ELECTRICAL_CURRENT_AMPERE, - "ATTR_ENABLED": True, + "ST_ICON": "mdi:ev-station", + "ST_LABEL": "Max Available Power", + "ST_ROUND": 0, + "ST_UNIT": ELECTRICAL_CURRENT_AMPERE, + "ST_ENABLED": True, }, "charging_speed": { - "ATTR_ICON": "mdi:speedometer", - "ATTR_LABEL": "Charging Speed", - "ATTR_ROUND": 0, - "ATTR_UNIT": None, - "ATTR_ENABLED": True, + "ST_ICON": "mdi:speedometer", + "ST_LABEL": "Charging Speed", + "ST_ROUND": 0, + "ST_UNIT": None, + "ST_ENABLED": True, }, "added_range": { - "ATTR_ICON": "mdi:map-marker-distance", - "ATTR_LABEL": "Added Range", - "ATTR_ROUND": 0, - "ATTR_UNIT": LENGTH_KILOMETERS, - "ATTR_ENABLED": True, + "ST_ICON": "mdi:map-marker-distance", + "ST_LABEL": "Added Range", + "ST_ROUND": 0, + "ST_UNIT": LENGTH_KILOMETERS, + "ST_ENABLED": True, }, "added_energy": { - "ATTR_ICON": "mdi:battery-positive", - "ATTR_LABEL": "Added Energy", - "ATTR_ROUND": 2, - "ATTR_UNIT": ENERGY_KILO_WATT_HOUR, - "ATTR_ENABLED": True, + "ST_ICON": "mdi:battery-positive", + "ST_LABEL": "Added Energy", + "ST_ROUND": 2, + "ST_UNIT": ENERGY_KILO_WATT_HOUR, + "ST_ENABLED": True, }, "charging_time": { - "ATTR_ICON": "mdi:timer", - "ATTR_LABEL": "Charging Time", - "ATTR_ROUND": None, - "ATTR_UNIT": None, - "ATTR_ENABLED": True, + "ST_ICON": "mdi:timer", + "ST_LABEL": "Charging Time", + "ST_ROUND": None, + "ST_UNIT": None, + "ST_ENABLED": True, }, "cost": { - "ATTR_ICON": "mdi:ev-station", - "ATTR_LABEL": "Cost", - "ATTR_ROUND": None, - "ATTR_UNIT": None, - "ATTR_ENABLED": True, + "ST_ICON": "mdi:ev-station", + "ST_LABEL": "Cost", + "ST_ROUND": None, + "ST_UNIT": None, + "ST_ENABLED": True, }, "state_of_charge": { - "ATTR_ICON": "mdi:battery-charging-80", - "ATTR_LABEL": "State of Charge", - "ATTR_ROUND": None, - "ATTR_UNIT": PERCENTAGE, - "ATTR_ENABLED": True, + "ST_ICON": "mdi:battery-charging-80", + "ST_LABEL": "State of Charge", + "ST_ROUND": None, + "ST_UNIT": PERCENTAGE, + "ST_ENABLED": True, }, "current_mode": { - "ATTR_ICON": "mdi:ev-station", - "ATTR_LABEL": "Current Mode", - "ATTR_ROUND": None, - "ATTR_UNIT": None, - "ATTR_ENABLED": True, + "ST_ICON": "mdi:ev-station", + "ST_LABEL": "Current Mode", + "ST_ROUND": None, + "ST_UNIT": None, + "ST_ENABLED": True, }, "depot_price": { - "ATTR_ICON": "mdi:ev-station", - "ATTR_LABEL": "Depot Price", - "ATTR_ROUND": 2, - "ATTR_UNIT": None, - "ATTR_ENABLED": True, + "ST_ICON": "mdi:ev-station", + "ST_LABEL": "Depot Price", + "ST_ROUND": 2, + "ST_UNIT": None, + "ST_ENABLED": True, }, "status_description": { - "ATTR_ICON": "mdi:ev-station", - "ATTR_LABEL": "Status Description", - "ATTR_ROUND": None, - "ATTR_UNIT": None, - "ATTR_ENABLED": True, + "ST_ICON": "mdi:ev-station", + "ST_LABEL": "Status Description", + "ST_ROUND": None, + "ST_UNIT": None, + "ST_ENABLED": True, }, } diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index 1df8e29049b..8588b1e351e 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -12,6 +12,7 @@ from homeassistant.helpers.update_coordinator import ( from .const import CONF_CONNECTIONS, DOMAIN, SENSOR_TYPES CONF_STATION = "station" +UPDATE_INTERVAL = 30 _LOGGER = logging.getLogger(__name__) @@ -19,12 +20,12 @@ _LOGGER = logging.getLogger(__name__) async def wallbox_updater(wallbox, hass): """Get new sensor data for Wallbox component.""" - data = await hass.async_add_executor_job(wallbox.get_data) + data = await wallbox.async_get_data(hass) filtered_data = {k: data[k] for k in SENSOR_TYPES if k in data} for key, value in filtered_data.items(): - sensor_round = SENSOR_TYPES[key]["ATTR_ROUND"] + sensor_round = SENSOR_TYPES[key]["ST_ROUND"] if sensor_round: try: filtered_data[key] = round(value, sensor_round) @@ -48,7 +49,7 @@ async def async_setup_entry(hass, config, async_add_entities): name="wallbox", update_method=async_update_data, # Polling interval. Will only be polled if there are subscribers. - update_interval=timedelta(seconds=30), + update_interval=timedelta(seconds=UPDATE_INTERVAL), ) await coordinator.async_refresh() @@ -66,9 +67,9 @@ class WallboxSensor(CoordinatorEntity, Entity): """Initialize a Wallbox sensor.""" super().__init__(coordinator) self._properties = SENSOR_TYPES[ent] - self._name = f"{config.title} {self._properties['ATTR_LABEL']}" - self._icon = self._properties["ATTR_ICON"] - self._unit = self._properties["ATTR_UNIT"] + self._name = f"{config.title} {self._properties['ST_LABEL']}" + self._icon = self._properties["ST_ICON"] + self._unit = self._properties["ST_UNIT"] self._ent = ent @property diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py index 7a3929053ef..d6769509a6b 100644 --- a/tests/components/wallbox/test_config_flow.py +++ b/tests/components/wallbox/test_config_flow.py @@ -4,8 +4,7 @@ from unittest.mock import patch from voluptuous.schema_builder import raises from homeassistant import config_entries, data_entry_flow -from homeassistant.components.wallbox import config_flow -from homeassistant.components.wallbox.config_flow import CannotConnect, InvalidAuth +from homeassistant.components.wallbox import config_flow, CannotConnect, InvalidAuth from homeassistant.components.wallbox.const import DOMAIN from homeassistant.core import HomeAssistant @@ -27,7 +26,7 @@ async def test_form_invalid_auth(hass): ) with patch( - "homeassistant.components.wallbox.config_flow.WallboxHub.authenticate", + "homeassistant.components.wallbox.config_flow.WallboxHub.async_authenticate", side_effect=InvalidAuth, ): result2 = await hass.config_entries.flow.async_configure( @@ -50,7 +49,7 @@ async def test_form_cannot_connect(hass): ) with patch( - "homeassistant.components.wallbox.config_flow.WallboxHub.authenticate", + "homeassistant.components.wallbox.config_flow.WallboxHub.async_authenticate", side_effect=CannotConnect, ): result2 = await hass.config_entries.flow.async_configure( @@ -63,7 +62,7 @@ async def test_form_cannot_connect(hass): ) assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} + assert result2["errors"] == {"base": "invalid_auth"} async def test_validate_input(hass): @@ -96,7 +95,7 @@ async def test_validate_input(hass): async def test_configflow_class(): """Test configFlow class.""" - configflow = config_flow.ConfigFlow + configflow = config_flow.ConfigFlow() assert configflow with patch( @@ -105,14 +104,20 @@ async def test_configflow_class(): ), raises(Exception): assert await configflow.async_step_user(True) + with patch( + "homeassistant.components.wallbox.config_flow.validate_input", + side_effect=CannotConnect, + ), raises(Exception): + assert await configflow.async_step_user(True) + def test_cannot_connect_class(): """Test cannot Connect class.""" - cannot_connect = config_flow.CannotConnect + cannot_connect = CannotConnect assert cannot_connect def test_invalid_auth_class(): """Test invalid auth class.""" - invalid_auth = config_flow.InvalidAuth + invalid_auth = InvalidAuth assert invalid_auth diff --git a/tests/components/wallbox/test_init.py b/tests/components/wallbox/test_init.py index fdcfa49c73a..e0e3096b972 100644 --- a/tests/components/wallbox/test_init.py +++ b/tests/components/wallbox/test_init.py @@ -43,7 +43,6 @@ async def test_wallbox_setup_entry(hass: HomeAssistantType): async def test_wallbox_unload_entry(hass: HomeAssistantType): """Test Wallbox Unload.""" hass.data[DOMAIN] = {"connections": {entry.entry_id: entry}} - print(hass.data) assert await wallbox.async_unload_entry(hass, entry) @@ -59,7 +58,7 @@ async def test_wallbox_setup(hass: HomeAssistantType): assert await wallbox.async_setup(hass, entry) -def test_hub_class(): +async def test_hub_class(hass: HomeAssistantType): """Test hub class.""" station = ("12345",) @@ -79,18 +78,18 @@ def test_hub_class(): text='{"Temperature": 100, "Location": "Toronto", "Datetime": "2020-07-23", "Units": "Celsius"}', status_code=200, ) - assert hub.authenticate() - assert hub.get_data() + assert await hub.async_authenticate(hass) + assert await hub.async_get_data(hass) with requests_mock.Mocker() as m, raises(wallbox.InvalidAuth): m.get("https://api.wall-box.com/auth/token/user", text="data", status_code=403) - assert hub.authenticate() + assert await hub.async_authenticate(hass) with requests_mock.Mocker() as m, raises(ConnectionError): m.get("https://api.wall-box.com/auth/token/user", text="data", status_code=404) - assert hub.authenticate() + assert await hub.async_authenticate(hass) with requests_mock.Mocker() as m, raises(wallbox.InvalidAuth): m.get("https://api.wall-box.com/auth/token/user", text="data", status_code=403) @@ -99,7 +98,7 @@ def test_hub_class(): text="data", status_code=403, ) - assert hub.get_data() + assert await hub.async_get_data(hass) with requests_mock.Mocker() as m, raises(ConnectionError): m.get("https://api.wall-box.com/auth/token/user", text="data", status_code=404) @@ -108,4 +107,4 @@ def test_hub_class(): text="data", status_code=404, ) - assert hub.get_data() + assert await hub.async_get_data(hass) diff --git a/tests/components/wallbox/test_sensor.py b/tests/components/wallbox/test_sensor.py index 7c00807cf6c..5839d1ce1c5 100644 --- a/tests/components/wallbox/test_sensor.py +++ b/tests/components/wallbox/test_sensor.py @@ -1,9 +1,10 @@ """Test Wallbox Switch component.""" import json -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch +import requests_mock -from homeassistant.components.wallbox import sensor +from homeassistant.components.wallbox import sensor, WallboxHub from homeassistant.components.wallbox.const import CONF_STATION, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.typing import HomeAssistantType @@ -28,6 +29,12 @@ test_response_rounding_error = json.loads( '{"charging_power": "XX","max_available_power": "xx","charging_speed": 0,"added_range": "xx","added_energy": "XX"}' ) +station = ("12345",) +username = ("test-username",) +password = "test-password" + +wallbox = WallboxHub(station, username, password) + async def test_wallbox_sensor_class(): """Test wallbox sensor class.""" @@ -46,13 +53,31 @@ async def test_wallbox_sensor_class(): async def test_wallbox_updater(hass: HomeAssistantType): """Test wallbox updater.""" - wallbox = MagicMock(return_value=test_response) - wallbox.get_data = MagicMock(return_value=test_response) - await sensor.wallbox_updater(wallbox, hass) + with requests_mock.Mocker() as m: + m.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + status_code=200, + ) + m.get( + "https://api.wall-box.com/chargers/status/('12345',)", + json=test_response, + status_code=200, + ) + await sensor.wallbox_updater(wallbox, hass) async def test_wallbox_updater_rounding_error(hass: HomeAssistantType): """Test wallbox updater rounding error.""" - wallbox = MagicMock(return_value=test_response) - wallbox.get_data = MagicMock(return_value=test_response_rounding_error) - await sensor.wallbox_updater(wallbox, hass) + with requests_mock.Mocker() as m: + m.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + status_code=200, + ) + m.get( + "https://api.wall-box.com/chargers/status/('12345',)", + json=test_response_rounding_error, + status_code=200, + ) + await sensor.wallbox_updater(wallbox, hass)